useEffect 初始化 先来解读下几个参数:
fiberFlags
:有副作用的更新标记,用来标记hook所在的fiber;
hookFlags
:副作用标记;
create
:使用者传入的回调函数;
deps
:使用者传入的数组依赖;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 function mountEffectImpl ( fiberFlags: Flags, hookFlags: HookFlags, create: ( ) => (( ) => void ) | void , deps : Array <mixed > | void | null , ): void { const hook = mountWorkInProgressHook(); const nextDeps = deps === undefined ? null : deps; currentlyRenderingFiber.flags |= fiberFlags; hook.memoizedState = pushEffect( HookHasEffect | hookFlags, create, createEffectInstance(), nextDeps, ); } function createEffectInstance ( ): EffectInstance { return {destroy : undefined }; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 function pushEffect ( tag: HookFlags, create: ( ) => (( ) => void ) | void , inst : EffectInstance , deps : Array <mixed > | null , ): Effect { const effect: Effect = { tag, create, inst, deps, next: (null : any), }; let componentUpdateQueue: null | FunctionComponentUpdateQueue = (currentlyRenderingFiber.updateQueue: any); if (componentUpdateQueue === null ) { componentUpdateQueue = createFunctionComponentUpdateQueue(); currentlyRenderingFiber.updateQueue = (componentUpdateQueue: any); componentUpdateQueue.lastEffect = effect.next = effect; } else { const lastEffect = componentUpdateQueue.lastEffect; if (lastEffect === null ) { componentUpdateQueue.lastEffect = effect.next = effect; } else { const firstEffect = lastEffect.next; lastEffect.next = effect; effect.next = firstEffect; componentUpdateQueue.lastEffect = effect; } } return effect; }
上面这段代码除了初始化副作用的结构代码外,还有就是操作闭环链表,向链表末尾添加新的 effect
,该 effect.next
指向 fisrtEffect
,并且链表当前的指针指向最新添加的 effect
。
简单总结一下:给 hook
所在的 fiber
打上副作用更新标记,并且 fiber.memoizedState.hook.memoizedState
和 fiber.updateQueue
存储了相关的副作用,这些副作用通过闭环链表的结构存储。
更新 主要功能就是创建一个带有回调函数的 newHook
去覆盖之前的 hook
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 function updateEffectImpl ( fiberFlags: Flags, hookFlags: HookFlags, create: ( ) => (( ) => void ) | void , deps : Array <mixed > | void | null , ): void { const hook = updateWorkInProgressHook(); const nextDeps = deps === undefined ? null : deps; const effect: Effect = hook.memoizedState; const inst = effect.inst; if (currentHook !== null ) { if (nextDeps !== null ) { const prevEffect: Effect = currentHook.memoizedState; const prevDeps = prevEffect.deps; if (areHookInputsEqual(nextDeps, prevDeps)) { hook.memoizedState = pushEffect(hookFlags, create, inst, nextDeps); return ; } } } currentlyRenderingFiber.flags |= fiberFlags; hook.memoizedState = pushEffect( HookHasEffect | hookFlags, create, inst, nextDeps, ); }
create 和 destroy 函数是怎样被调用? 在弄清楚怎样被调用之前,我们先来看 destroy
函数的存在性问题。即:在 hook
的 mount
阶段,我们创建 effect
对象的时候,destroy
函数是不存在的。因为,destroy
函数本来就是 create
函数的返回值。而此时 create
函数还没有被调用。
而我们知道,组件 mount
阶段过后, create
函数是一定会被调用的。所以,我们也可以推理得出,在组件的 update
阶段,effect
对象的 create
函数和 destroy
函数肯定是存在的(现在我们假设用户一定定义 destroy
函数 )。
react
会在 commit
阶段去调用我们的 create
函数和 destroy
函数。commit
阶段又可以分为三个子阶段:
beforeMutation
mutation
layout
回到源码的视角,create
函数和 destroy
函数调用具体是发生在 commit
阶段的入口函数 commitRootImpl
内部,而真正的调用入口函数为 flushPassiveEffects
。
调用入口 该函数在 commitRootImpl
出现了三次。也就是说 create
函数和 destroy
函数调用存在三个入口:
处理由于调用 flushSyncUpdateQueue()
所衍生的 effect
在 beforeMutation
子阶段之前的对执行 effect
进行调度
在 layout
子阶段完成之后的同步调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 function commitRootImpl ( root: FiberRoot, recoverableErrors: null | Array<CapturedValue<mixed>>, transitions: Array<Transition> | null, didIncludeRenderPhaseUpdate: boolean, renderPriorityLevel: EventPriority, spawnedLane: Lane, ) { do { flushPassiveEffects(); } while (rootWithPendingPassiveEffects !== null ); if ( (finishedWork.subtreeFlags & PassiveMask) !== NoFlags || (finishedWork.flags & PassiveMask) !== NoFlags ) { if (!rootDoesHavePassiveEffects) { rootDoesHavePassiveEffects = true ; pendingPassiveEffectsRemainingLanes = remainingLanes; pendingPassiveTransitions = transitions; scheduleCallback(NormalSchedulerPriority, () => { flushPassiveEffects(); return null ; }); } } const subtreeHasEffects = (finishedWork.subtreeFlags & (BeforeMutationMask | MutationMask | LayoutMask | PassiveMask)) !== NoFlags; const rootHasEffect = (finishedWork.flags & (BeforeMutationMask | MutationMask | LayoutMask | PassiveMask)) !== NoFlags; if (subtreeHasEffects || rootHasEffect) { executionContext |= CommitContext; const shouldFireAfterActiveInstanceBlur = commitBeforeMutationEffects( root, finishedWork, ); commitMutationEffects(root, finishedWork, lanes); commitLayoutEffects(finishedWork, root, lanes); } if (includesSyncLane(pendingPassiveEffectsLanes) && root.tag !== LegacyRoot) { flushPassiveEffects(); } }
通过简单的 react
应用来测试得到的结果是:1)在组件的 mount
阶段,react
会走第二个入口;2)在组件的 update
阶段,react
会走第三个入口
不管是从哪个入口进入,create 函数和 destroy 函数调用都是发生了 DOM 更新之后
不管是从哪个入口进入,它们都是走同一个函数 flushPassiveEffects
1 2 3 4 5 6 7 8 9 10 11 function flushPassiveEffectsImpl ( ) { const prevExecutionContext = executionContext; executionContext |= CommitContext; commitPassiveUnmountEffects(root.current); commitPassiveMountEffects(root, root.current, lanes, transitions); }
且有相同的调用栈:
因为调用 destroy
和 调用 create
函数是分开的。所以,我们需要将两者分开来讨论。但是由于两者的调用逻辑几乎是一样的。所以,在这里,我们以调用 destroy
函数为例即可,create
函数的调用原理跟这个是一样的。 通过源码的阅读和调试,我将「调用 destroy
函数的过程」划分为两个步骤:
遍历 fiber
树 - 深度优先遍历 fiber
树,找到身上有 effect
的 fiber
节点
遍历 effect
链表 - 遍历当前的 effect
链表,根据当前 effect
是否满足特定的条件(是否包含特定标签)来确定是否要调用 destroy
函数。
1. 遍历 fiber 树 我们也知道,所有的 useEffect hook
函数只能用 function component
里面使用。
而 fiber
上可能存在多个 function component
类型的 fiber
节点使用了 hook
函数。所以,这一步中,遍历 fiber
树的目的就是要找到消费了 hook
函数的 fiber
节点。
在这一步之前,react
其实是有做一些前置工作的。那就是:因为遍历 fiber
都是深度优先,在 render
阶段,performUnitOfWork
中的 beginWork
执行到叶子节点后会进入到 completeUnitOfWork
中,会执行 bubbleProperties
,也就是 react
会逐步向上追溯当前消费了 hook
函数的 fiber
节点的所有的祖先 fiber
节点,一一给它们的 subtreeFlags
属性值加入一个 Passive
标签。这种机制类似于浏览器的事件冒泡。浏览器的事件起源于特定的 DOM
节点,但是它会在冒泡阶段向上传播到 document
这个根节点。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 function completeWork ( current: Fiber | null, workInProgress: Fiber, renderLanes: Lanes, ): Fiber | null { const newProps = workInProgress.pendingProps; popTreeContext(workInProgress); switch (workInProgress.tag) { case IndeterminateComponent: case LazyComponent: case SimpleMemoComponent: case FunctionComponent: case ForwardRef: case Fragment: case Mode: case Profiler: case ContextConsumer: case MemoComponent: bubbleProperties(workInProgress); return null ; case ClassComponent: { const Component = workInProgress.type; if (isLegacyContextProvider(Component)) { popLegacyContext(workInProgress); } bubbleProperties(workInProgress); return null ; } } } function bubbleProperties (completedWork: Fiber ) { const didBailout = completedWork.alternate !== null && completedWork.alternate.child === completedWork.child; let newChildLanes = NoLanes; let subtreeFlags = NoFlags; if (!didBailout) { if (enableProfilerTimer && (completedWork.mode & ProfileMode) !== NoMode) { let actualDuration = completedWork.actualDuration; let treeBaseDuration = ((completedWork.selfBaseDuration: any): number); let child = completedWork.child; while (child !== null ) { newChildLanes = mergeLanes( newChildLanes, mergeLanes(child.lanes, child.childLanes), ); subtreeFlags |= child.subtreeFlags; subtreeFlags |= child.flags; actualDuration += child.actualDuration; treeBaseDuration += child.treeBaseDuration; child = child.sibling; } completedWork.actualDuration = actualDuration; completedWork.treeBaseDuration = treeBaseDuration; } else { let child = completedWork.child; while (child !== null ) { newChildLanes = mergeLanes( newChildLanes, mergeLanes(child.lanes, child.childLanes), ); subtreeFlags |= child.subtreeFlags; subtreeFlags |= child.flags; child.return = completedWork; child = child.sibling; } } completedWork.subtreeFlags |= subtreeFlags; } else { if (enableProfilerTimer && (completedWork.mode & ProfileMode) !== NoMode) { let treeBaseDuration = ((completedWork.selfBaseDuration: any): number); let child = completedWork.child; while (child !== null ) { newChildLanes = mergeLanes( newChildLanes, mergeLanes(child.lanes, child.childLanes), ); subtreeFlags |= child.subtreeFlags & StaticMask; subtreeFlags |= child.flags & StaticMask; treeBaseDuration += child.treeBaseDuration; child = child.sibling; } completedWork.treeBaseDuration = treeBaseDuration; } else { let child = completedWork.child; while (child !== null ) { newChildLanes = mergeLanes( newChildLanes, mergeLanes(child.lanes, child.childLanes), ); subtreeFlags |= child.subtreeFlags & StaticMask; subtreeFlags |= child.flags & StaticMask; child.return = completedWork; child = child.sibling; } } completedWork.subtreeFlags |= subtreeFlags; } completedWork.childLanes = newChildLanes; return didBailout; }
回到 useEffect
这个主题。上面所提到的前置工作中,有一点特别需要注意的点是:如果「消费了 hook
函数的那个 fiber
节点」的子树之下没有其他消费了 hook
函数的 fiber
节点,它自己的 subtreeFlags
属性值是不会被贴上一个 Passive
标签的。这也不难理解,因为这恰恰是符合 subtreeFlags
属性名的语义的。
假设只有我们的 <Counter />
组件里面消费到 useEffect
这个 hook
函数。那么,在 render
阶段,经过上面所提到的前置工作后,我们会得到这样的一颗 fiber
树:
好了,做完上面的前置工作后,react
会在 commit
阶段发起对 flushPassiveEffects
的调度,将任务加入 taskQueue
中,在适当的时间切片内会执行到此调度。
这个遍历工作是从 fiber
树的根节点 - hostRootFiber
开始的。
源码中的 commitPassiveUnmountOnFiber()
, recursivelyTraversePassiveUnmountEffects()
和 commitHookPassiveUnmountEffects()
这三个函数共同完成了这个过程。而这三个函数是有分工的:
commitPassiveUnmountOnFiber() - 是遍历流程的入口
recursivelyTraversePassiveUnmountEffects() - 负责实现了以深度优先的算法的「递」与「归」
commitHookPassiveUnmountEffects() - 在「归」之前,尝试去调用当前 fiber 节点 effect 链表上的所有的 destroy 函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 function my_commitPassiveUnmountOnFiber (parentFiber ) { if (parentFiber.subtreeFlags & PassiveMask) { const child = parentFiber.child; while (child !== null ) { my_commitPassiveUnmountOnFiber(child); child = child.sibling; } } const isfunctionComponentKind = [FunctionComponent, ForwardRef, SimpleMemoComponent].includes(parentFiber.tag); const hasPassiveEffect = parentFiber.flags & Passive if (isfunctionComponentKind && hasPassiveEffect){ commitHookPassiveUnmountEffects( parentFiber, parentFiber.return, Passive | HasEffect ); } }
其实看完 react
源码,熟悉 react
内部的原理后,这种深优先遍历算法跟 render
阶段的 「work-loop」的遍历算法是一模一样的。唯一的一个区别点有两点:
它深度优先遍历不一定会遍历到当前子树路径的叶子节点。它会因为不满足「以当前 parentFiber 为根节点的子树上是否有身上贴有 Passive 标签的 fiber 节点?」这个条件提前终止了;
跟 render
阶段的 「work-loop」的遍历算法中,归去之前必定会执行 complete work
不同,当前的遍历算法会先检查是否满足条件,如果不满足,则不执行 “complete work” 。显然,这里的 “complete work” 是指 commitHookPassiveUnmountEffects()
的调用。
2. 遍历 effect 链表 上面,我们已经介绍了 react
是如何了遍历整棵 fiber
树,找到那些所有需要执行 effect
的 fiber
节点。上一步骤末尾,我们也指出了,执行 effect
的 fiber
节点的函数是 commitHookPassiveUnmountEffects()
。而 commitHookPassiveUnmountEffects()
会直接调用 commitHookEffectListUnmount()
。顾名思义,该函数就是 react
遍历 effect
链表去调用 destroy
函数的所在。
effect 链表并不是只存储 useEffect hook 函数的 effect 对象
那这里所提到的 「effect 类型的」hook 函数有哪些呢?从 react@18.2.0 的源码来看,它包括了5 种类型的 hook
函数:
useEffect()
useLayoutEffect()
useInsertionEffect()
useSyncExternalStore()
useImperativeHandle()
提出这个结论的依据是: 这些 hook 函数的调用栈最终都是指向了 pushEffect() 函数。如下图:
而 pushEffect()
函数就是负责创建新的 effect
对象,把它追加到当前 fiber
节点 effect
链表的尾部。可见,如果一个 function component
都消费了上面这些 hook
函数的话,那么它所对应的 fiber
节点的 effect
链表上并不是单纯包含 useEffect hook
函数所产生的 effect
。 回到正题。上面我们指出了 commitHookEffectListUnmount
函数就是 react
遍历 effect
链表去调用 destroy
函数的所在,下面来看看它的源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 function commitHookEffectListUnmount ( flags, finishedWork, nearestMountedAncestor ) { const updateQueue = finishedWork.updateQueue; const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null ; if (lastEffect !== null ) { const firstEffect = lastEffect.next; let effect = firstEffect; do { if ((effect.tag & flags) === flags) { const destroy = effect.destroy; effect.destroy = undefined ; if (destroy !== undefined ) { safelyCallDestroy(finishedWork, nearestMountedAncestor, destroy); } } effect = effect.next; } while (effect !== firstEffect); } } function safelyCallDestroy (current, nearestMountedAncestor, destroy ) { try { destroy(); } catch (error) { captureCommitPhaseError(current, nearestMountedAncestor, error); } }
有了上面的铺垫,我们很快就能看明白这段代码的意思。react
通过 do{...}while()
循环来遍历 effect
链表。我们在上面也提到过,effect
链表其实是单向的循环链表。所以,当前即将需要遍历的 effect
对象又指会了第一个 effect
的时候,意味着我们已经遍历完了整条链表,可以退出循环了。
小结
执行顺序 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 import {useEffect} from "react" ;function Child ( ) { useEffect(function childEffect ( ) { console .log('Child effect been called' ); }); return null } function Sibling ( ) { useEffect(function siblingEffect ( ) { console .log('Sibling effect been called' ); }); return null ; } function App ( ) { useEffect(function appEffect ( ) { console .log('App effect been called' ); }); return ( <div> <Child /> <Sibling /> </div> ) }
相信看完上面的分析,也不难得出答案:
1 2 3 Child effect been called Sibling effect been called App effect been called
react 采用的递归遍历算法是深度优先算法。示例 react 应用所对应的 fiber 树如下:
用文字描述整个递归遍历过程是这样的:<App>
的递;<div>
的递;<Child>
的递;<Child>
的归;<Sibling>
的递;的归;
``<App>
的归。
在归时候,react 会在当前 fiber 节点上调用 commitHookEffectListMount() 函数,所以,调用结果是:
在 <Child>
身上调用 commitHookEffectListMount()
; 在 <Sibling>
身上调用 commitHookEffectListMount()
; 在 <App>
身上调用 commitHookEffectListMount()
。
useLayoutEffect useLayoutEffect
函数跟 useEffect
hook 函数 的不同。两者的不同之处在于两点:
它们给「自己所创建的 effect 对象」所贴的 effect flag
是不同的;
它们给「自己所关联的 fiber 节点」所追加的 fiber flag
的不同的。
所贴的 hook flag 不同
对于 useLayoutEffect() hook
函数而言,它给自己所创建的 effect
对象所贴的 effect flag
是 Layout
而 useEffect() hook
函数给自己所创建的 effect
对象所贴的 effect flag
是 Passive
;
所追加的 fiber flag 不同 对于 useEffect() hook
函数而言,它给自己所关联的 fiber
节点所追加的 fiber flag
是:
mount
阶段是 Passive | PassiveStatic
update
阶段是 Passive
而 useLayoutEffect() hook
函数给自己所关联的 fiber
节点所追加的 fiber flag
是:
mount
阶段是 Update | LayoutStatic
update
阶段是 Update
useLayoutEffect - mutation 子阶段调用 destroy 函数 它的 destroy
函数的调用是发生在 mutation
子阶段。具体来讲,就是发生在 commitMutationEffectsOnFiber()
这个函数里面:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 function commitMutationEffectsOnFiber (finishedWork, root, lanes ) { const current = finishedWork.alternate; const flags = finishedWork.flags; switch (finishedWork.tag) { case FunctionComponent: case ForwardRef: case MemoComponent: case SimpleMemoComponent: { recursivelyTraverseMutationEffects(root, finishedWork); commitReconciliationEffects(finishedWork); if (flags & Update) { { try { commitHookEffectListUnmount( Layout | HasEffect, finishedWork, finishedWork.return ); } catch (error) { captureCommitPhaseError(finishedWork, finishedWork.return, error); } } } return ; } } }
useLayoutEffect - layout 子阶段调用 create 函数
hook 类型 - 这里就是指 useLayoutEffect() hook 的 create 函数;
class 类型 - 这里是指 class component 的两个生命周期函数:
componentDidMount()
componentDidUpdate()
useLayoutEffect() hook
的 create
函数的真正调用入口为 commitHookLayoutEffects()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 function commitLayoutEffectOnFiber ( finishedRoot, current, finishedWork, committedLanes ) { const flags = finishedWork.flags; switch (finishedWork.tag) { case FunctionComponent: case ForwardRef: case SimpleMemoComponent: { recursivelyTraverseLayoutEffects( finishedRoot, finishedWork, committedLanes ); if (flags & Update) { commitHookLayoutEffects(finishedWork, Layout | HasEffect); } break ; } } }
应用 关于这个问题,react 官方文档给出了一个使用用例: 在浏览器重绘之前测量 DOM 的布局信息 。
「界面更新时候的闪烁问题」。为什么使用 useLayoutEffect()
能解决这类的问题呢?
首先,我们要知道为什么会造成闪烁。造成闪烁的原因是 react
在短时间更新了多帧显示内容不一样的界面。而 useLayoutEffect() hook
的 create
函数是在 layout
子阶段以同步的,批量的方式去执行的。也就是说,create
函数里面的所发起的多次状态更新请求只会产生一次的实质性的界面重绘。 通过抹除代表着中间状态的过渡帧,将多帧压缩为一帧来更新界面,这就是 useLayoutEffect()
能解决「界面更新时候的闪烁问题」原因之所在。
上面所提到的 「界面更新时候的闪烁问题」只是 useLayoutEffect()
能解决问题中的一个垂类。useLayoutEffect() 应该还能解决更多不同业务场景下的问题,我们得通过现象看本质。
那 useLayoutEffect()
的本质能力是什么?
访问更新后的 DOM
树 - 因为 useLayoutEffect() hook
的 create
函数是在 mutation
子阶段之后的 layout
子阶段执行的。所以,这就意味着我们可以去访问更新后的 DOM
树和做一些布局信息的测量。
同步/批量执行状态更新,阻塞浏览器重绘 - react
将会以「同步阻塞,批量更新」的方式去对待 create
函数体里面所发起的过个状态更新请求。也就说,在 create
函数体里面所发起的过个状态更新请求只会产生一次界面重绘。
总结 从源码的角度来看, 这两者其实都是在同一个架构里面,拥有很多相同点:
每一个 effect
类型 hook
都会关联一个 hook
对象 和 effect
对象;
同一个 function component
内,所有的 hook
对象都共用同一条 hook
链表;
同一个 function component
内,所有的 effect
对象都共用同一条 effect
链表;
同一个调用机制 - 在 render
阶段创建 hook
对象和 effect
对象,在 commit
阶段去调用 hook
的 create
和 destroy
函数。
这两者的不同点在于:
useEffect()
的 destroy
和 create
函数是在同一个子阶段(layout
子阶段后的 passive
子阶段)调用的(调用入口要么是调度后的 flushPassiveEffectImpl()
,要么是同步的flushPassiveEffectImpl()
);
useLayoutEffect()
的这两个函数却是在不同的子阶段执行。destroy
是在 mutation
子阶段执行的,而 create
是在 layout
子阶段。