一. 往期文章推荐
1.1 手写mini React,理解React渲染原理
1.2 手写React useState,理解useState原理
二. useEffect方法介绍
useEffect方法接收两个参数,第一个是执行函数create,第二个是依赖deps,在首次渲染时会执行一次create方法,在下次渲染时会比对deps值是否变更,如果有会再次执行create方法
我们可以在create方法里返回一个函数destroy,destroy方法会在deps值发生变化或组件卸载时执行。
例如下面这段代码,在首次渲染时控制台会输出HelloWorld Mount、App Mount,在点击click按钮时将visible设置为false,会触发更新渲染,控制台会输出HelloWorld Unmount
javascript
function HelloWorld() {
useEffect(() => {
console.log('HelloWorld Mount')
return () => {
console.log('HelloWorld Unmount')
}
}, [])
return <h1>hello world</h1>
}
function App() {
const [visible, useVisible] = useState(true)
useEffect(() => {
console.log('App Mount')
}, [])
return (
<div>
<button onClick={() => setVisible(!visible)}>click</button>
{visible && <HelloWorld />}
</div>
)
}
三. 实现useEffect
3.1 定义Hook对象原型
每次调用useEffect方法时都会创建一个hook对象,多个hook对象通过next指针索引,构建单链表数据结构
javascript
function Hook() {
this.memoizedState = null // 记录hook数据
this.next = null // 记录下一个hook
this.queue = [] // 收集更新state方法
}
3.2 修改FiberNode对象原型
新增updateQueue属性,记录useEffect数据,如执行方法create,依赖deps和调用create方法返回的destroy方法
javascript
function FiberNode() {
this.updateQueue = null // 记录useEffect数据
}
3.3 定义函数组件方法调用装饰器
当每次调用函数组件方法(例如App Compoent Function)时会执行renderWithHooks方法,记录新FiberNode节点、旧FiberNode节点的hook链表节点,在调用useEffect方法时会用到
javascript
// 记录新FiberNode节点
let currentlyRenderingFiber = null
// 记录旧FiberNode节点的hook链表节点
let currentHook = null
// 记录新FiberNode节点hook链表节点
let workInProgressHook = null
/**
* @param {*} current 旧FiberNode节点
* @param {*} workInProgress 新FiberNode节点
* @param {*} Component 函数组件方法
* @param {*} props 函数组件方法入参属性
*/
export function renderWithHooks(current, workInProgress, Component, props) {
// 记录新FiberNode节点
currentlyRenderingFiber = workInProgress
if (current !== null) {
// 记录旧FiberNode节点的hook链表
currentHook = current.memoizedState
}
workInProgress.updateQueue = null
// 调用组件方法获取child ReactElement
const children = Component(props)
currentlyRenderingFiber = null
currentHook = null
workInProgressHook = null
return children
}
3.4 首次调用useEffect方法
当首次执行函数组件方法,调用useEffect方法时会执行mountEffect方法逻辑,创建hook对象,将useEffect的入参赋值给hook的memoizedState属性和FiberNode节点的updateQueue属性
javascript
function mountWorkInProgressHook() {
const hook = new Hook()
// 构建hook链表
if (workInProgressHook === null) {
currentlyRenderingFiber.memoizedState = workInProgressHook = hook
} else {
workInProgressHook = workInProgressHook.next = hook
}
return hook
}
function pushEffect(create, deps, destroy = null) {
const effect = { create, deps, destroy }
// 将effect添加到FiberNode节点updateQueue属性中,在更新DOM阶段执行
if (currentlyRenderingFiber.updateQueue === null)
currentlyRenderingFiber.updateQueue = []
const queue = currentlyRenderingFiber.updateQueue
queue.push(effect)
return effect
}
function mountEffect(create, deps) {
// 创建hook对象,构建hook单链表
const hook = mountWorkInProgressHook()
// 将当前FiberNode节点的flags赋值为Passive,flags属性表示副作用,例如更新,删除等,在更新DOM阶段会根据flags属性值执行对应的副作用逻辑
currentlyRenderingFiber.flags |= Passive
hook.memoizedState = pushEffect(create, deps)
}
3.5 调用useEffect create方法
递归遍历FiberNode节点,判断flags属性值是否有Passive,如果有则遍历该节点updateQueue属性值,调用useEffect的create方法
javascript
// 遍历调用useEffect的create方法,获取destroy方法
function commitHookPassiveMountEffects(finishWork) {
const queue = finishWork.updateQueue
queue.forEach((effect) => {
effect.destroy = effect.create()
})
}
function recursivelyTraversePassiveMountEffects(finishWork) {
if (finishWork.subtreeFlags & Passive) {
let child = finishWork.child
while (child !== null) {
commitPassiveMountOnFiber(child)
child = child.sibling
}
}
}
function commitPassiveMountOnFiber(finishWork) {
switch (finishWork.tag) {
case FunctionComponent: {
recursivelyTraversePassiveMountEffects(finishWork)
if (finishWork.flags & Passive) {
// 调用useEffeact的create方法
commitHookPassiveMountEffects(finishWork)
}
break
}
default: {
recursivelyTraversePassiveMountEffects(finishWork)
break
}
}
}
3.6 更新调用useEffect方法
当触发更新渲染重新调用useEffect方法时,会比对deps是否变更,如果没有变更则不需要将FiberNode节点flags属性赋值为Passive,即在更新DOM阶段不会执行useEffect create方法,如果deps变更则需要将flags属性赋值为Passive
javascript
// 比对deps属性值是否变更
function areHookInputsEqual(nextDeps, prevDeps) {
for (let i = 0; i < nextDeps.length; i++) {
if (!Object.is(nextDeps[i], prevDeps[i])) {
return false
}
}
return true
}
function updateEffect(create, deps) {
const hook = mountWorkInProgressHook()
// 获取旧useEffect入参数据
const effect = currentHook.memoizedState
if (deps !== null && areHookInputsEqual(deps, effect.deps)) {
hook.memoizedState = pushEffect(create, deps, effect.destroy)
return
}
currentlyRenderingFiber.flags |= Passive
hook.memoizedState = pushEffect(create, deps)
}
3.7 更新调用useEffect destory方法
递归遍历FiberNode节点,判断flags属性值是否有Passive,如果有则遍历该节点updateQueue属性中的effect,调用useEffect的destroy方法
如果FiberNode节点的deletions属性不为空,说明有child FiberNode节点被删除,则递归遍历FiberNode节点deletions属性中的child FiberNode节点,调用对应useEffect的destroy方法
javascript
// 遍历调用useEffect的destroy方法,将effect的destroy属性赋值为null
function commitHookPassiveUnmountEffects(finishWork) {
const queue = finishWork.updateQueue
if (queue !== null) {
queue.forEach((effect) => {
if (effect.destroy) {
const destroy = effect.destroy
effect.destroy = null
destroy()
}
})
}
}
function recursivelyTraversePassiveUnmountEffects(finishWork) {
if (finishWork.deletions !== null) {
// 采用深度优先遍历算法,优先执行分支叶子节点useEffect的destroy方法
for (let i = 0; i < finishWork.deletions.length; i++) {
let fiber = finishWork.deletions[i]
while (true) {
let nextChild = fiber.child
while (nextChild !== null) {
fiber = nextChild
nextChild = nextChild.child
}
commitHookPassiveUnmountEffects(fiber)
if (fiber.sibling !== null) {
nextChild = fiber.sibling
fiber.sibling = null
} else {
if (fiber === finishWork.deletions[i]) break
fiber = fiber.return
fiber.child = null
}
}
}
}
if (finishWork.subtreeFlags & (Passive | ChildDeletion)) {
let child = finishWork.child
while (child !== null) {
commitPassiveUnmountOnFiber(child)
child = child.sibling
}
}
}
function commitPassiveUnmountOnFiber(finishWork) {
switch (finishWork.tag) {
case FunctionComponent: {
recursivelyTraversePassiveUnmountEffects(finishWork)
if (finishWork.flags & Passive) {
commitHookPassiveUnmountEffects(finishWork)
}
break
}
default: {
recursivelyTraversePassiveUnmountEffects(finishWork)
break
}
}
}
3.8 定义useEffect方法
如果新节点不存在旧FiberNode节点,说明是首次调用函数组件方法,则调用mountEffect方法,否则调用updateEffect方法
javascript
function useEffect(create, deps = null) {
const current = currentlyRenderingFiber.alternate
if (current === null) {
mountEffect(create, deps)
} else {
updateEffect(create, deps)
}
}