代码以React v19.1.0为例,https://github.com/facebook/react/tree/v19.1.0
上一篇我们深入了解了React的hooks的原理,包括它是以什么样的形式被React记录的,怎么样被执行的,并且还以useReducer/useState为例,学习了我们最常用到的一个hooks的执行逻辑,并且。
那么今天我们再来看看我们另一个也非常常用的hooks,useEffect
useEffect mountEffect 有了上一篇的经验,我们知道了hooks的实现主要在mountxxx和updatexxx方法里,那么我们先看看useEffect的mount逻辑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 function mountEffectImpl ( fiberFlags : Flags , hookFlags : HookFlags , create : () => (() => void ) | void , createDeps : Array <mixed> | void | null , update ?: ((resource: {...} | void | null ) => void ) | void , updateDeps ?: Array <mixed> | void | null , destroy ?: ((resource: {...} | void | null ) => void ) | void , ): void { const hook = mountWorkInProgressHook (); const nextDeps = createDeps === undefined ? null : createDeps; currentlyRenderingFiber.flags |= fiberFlags; hook.memoizedState = pushSimpleEffect ( HookHasEffect | hookFlags, createEffectInstance (), create, nextDeps, ); }
其中,mountWorkInProgressHook上一篇我们已经看过了,就是把这个hook插入到Fiber.memoizedState这个链表尾部
而nextDeps就来自我们调用useEffect时传入的依赖数组
给currentlyRenderingFiber打上PassiveEffect | PassiveStaticEffect 这两个flag,这样在commit阶段,React就知道这个Fiber上有副作用需要执行。而没有相应flag的Fiber节点就会被跳过来作为优化。详细逻辑要等后面讲commit阶段的文章。
接下来,通过pushSimpleEffect生成一个effect对象,并且将这个effect挂到Fiber.updateQueue这个链表节点上,保存起来。你的函数组件里每有一个useEffect,那组件对应的Fiber的updateQueue链表里就多一个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 31 32 33 34 35 36 37 38 function pushSimpleEffect ( tag : HookFlags , inst : EffectInstance , create : () => (() => void ) | void , createDeps : Array <mixed> | void | null , update ?: ((resource: {...} | void | null ) => void ) | void , updateDeps ?: Array <mixed> | void | null , destroy ?: ((resource: {...} | void | null ) => void ) | void , ): Effect { const effect : Effect = { tag, create, deps : createDeps, inst, next : (null : any ), }; return pushEffectImpl (effect); }function pushEffectImpl (effect : Effect ): Effect { let componentUpdateQueue : null | FunctionComponentUpdateQueue = (currentlyRenderingFiber.updateQueue : any ); if (componentUpdateQueue === null ) { componentUpdateQueue = createFunctionComponentUpdateQueue (); currentlyRenderingFiber.updateQueue = (componentUpdateQueue : any ); } 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; }
updateEffect 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 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 = pushSimpleEffect ( hookFlags, inst, create, nextDeps, ); return ; } } } currentlyRenderingFiber.flags |= fiberFlags; hook.memoizedState = pushSimpleEffect ( HookHasEffect | hookFlags, inst, create, nextDeps, ); }
可见这段逻辑依旧是很简单啊,中间有个判断依赖项是否发生变化的逻辑, 很简单的遍历数组比较。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 function areHookInputsEqual ( nextDeps : Array <mixed>, prevDeps : Array <mixed> | null , ): boolean { if (prevDeps === null ) { return false ; } for (let i = 0 ; i < prevDeps.length && i < nextDeps.length ; i++) { if (is (nextDeps[i], prevDeps[i])) { continue ; } return false ; } return true ; }
如果依赖项没发生变化,那么我们都知道这个effect是不会被执行的。所以我们看下区别,就是如果依赖项没发生变化,那么effect对象的hookFlags字段就不会包含HookHasEffect这个值,这会导致最终commit阶段不会执行这个effect。
最终执行effect 其实上面两个方法是调用useEffect的时候发生的事,但是我们都知道,盗用useEffect是在渲染过程中,执行函数组件函数体时候发生的事。但是里面的effect真正执行,是在渲染完成后。上面方法也可以看到,渲染过程中只是把effect挂到fiber上,并且打上对应的flag。
真的执行,在flushPassiveEffectsImpl方法里。
首先,需要递归执行上一次渲染的effect的return方法,在commitHookEffectListUnmount方法里,里面的destroy就是我们写的effect里面的return方法。
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 export function commitHookEffectListUnmount ( flags : HookFlags , finishedWork : Fiber , nearestMountedAncestor : Fiber | null , ) { try { const updateQueue : FunctionComponentUpdateQueue | null = (finishedWork.updateQueue : any ); const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null ; if (lastEffect !== null ) { const firstEffect = lastEffect.next ; let effect = firstEffect; do { if ((effect.tag & flags) === flags) { const inst = effect.inst ; const destroy = inst.destroy ; if (destroy !== undefined ) { inst.destroy = undefined ; safelyCallDestroy (finishedWork, nearestMountedAncestor, destroy); } } effect = effect.next ; } while (effect !== firstEffect); } } catch (error) { captureCommitPhaseError (finishedWork, finishedWork.return , error); } }
然后,递归的执行组件的这一次渲染的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 31 export function commitHookEffectListMount ( flags : HookFlags , finishedWork : Fiber , ) { try { const updateQueue : FunctionComponentUpdateQueue | null = (finishedWork.updateQueue : any ); const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null ; if (lastEffect !== null ) { const firstEffect = lastEffect.next ; let effect = firstEffect; do { if ((effect.tag & flags) === flags) { let destroy; const create = effect.create ; const inst = effect.inst ; destroy = create (); inst.destroy = destroy; } effect = effect.next ; } while (effect !== firstEffect); } } catch (error) { captureCommitPhaseError (finishedWork, finishedWork.return , error); } }
其中我们都可以注意到,有if ((effect.tag & flags) === flags)这个判断,flags的值就是HookPassive | HookHasEffect。所以如果依赖项没发生变化,那么effect.tag是不会包含HookHasEffect的,就会被跳过执行。所以这就实现了仅在依赖项发生变化时执行的功能
一些感悟 希望这篇文章能为大家解开useEffect的神秘面纱。虽然useEffect可能不如useReducer/useState那么常用,但是我打赌它一定是出问题最多的hooks。我刚学React的时候,也有过许多的疑惑,比如它的依赖项,比如它的执行时机等等。
就比如我之前就想过,它的依赖项一定要是useState返回的值吗?那么这篇文章看完之后其实我们也就有了答案,对于依赖项的值,没有任何约束。只要重新渲染了,他就会严格比较依赖项是否发生变化,不论依赖项是从哪里来的值。只不过前提是能触发重新渲染哦。所以这里也要纠正一个对useEffect的常见误区,用它来监听某个变量发生变化时执行操作。useEffect并不是在依赖项发生变化时执行,而是在重新渲染的时候才有可能执行。如果你的依赖项是一个自行定义的变量,或者useRef定义的变量,那么useEffect是起不到监听效果的呦