您现在的位置是:首页 >技术交流 >React 的源码与原理解读(十五):Hooks解读之四 useLayoutEffect&useEffect网站首页技术交流
React 的源码与原理解读(十五):Hooks解读之四 useLayoutEffect&useEffect
写在专栏开头(叠甲)
-
作者并不是前端技术专家,也只是一名喜欢学习新东西的前端技术小白,想要学习源码只是为了应付急转直下的前端行情和找工作的需要,这篇专栏是作者学习的过程中自己的思考和体会,也有很多参考其他教程的部分,如果存在错误或者问题,欢迎向作者指出,作者保证内容 100% 正确,请不要将本专栏作为参考答案。
-
本专栏的阅读需要你具有一定的 React 基础、 JavaScript 基础和前端工程化的基础,作者并不会讲解很多基础的知识点,例如:babel 是什么,jsx 的语法是什么,需要时请自行查阅相关资料。
-
本专栏很多部分参考了大量其他教程,若有雷同,那是作者抄袭他们的,所以本教程完全开源,你可以当成作者对各类教程进行了整合、总结并加入了自己的理解。
本一节的内容
这个章节主要讲解React 的 useLayoutEffect 和 useEffect 这两个 api,它们也是我们常用的 hooks,这两个 api 常被我们用来作为函数组件的生命周期来使用,用于处理我们副作用相关的内容,这篇我们就来看看他们的原理,以及在源码中运行作用的方式
useEffect 的定义
useEffect
是我们最常见的几个 hooks 之一,给函数组件增加了操作副作用的能力。
其第一个参数是一个副作用函数,React 会在每次渲染后调用副作用函数 ,副作用函数还可以通过返回一个函数来指定如何清除副作用。
其第二个参数是一个依赖项数组,只有其中有一项发生变化的情况才会触发当前的 userEffect ,否则不触发,如果我们设置第二个参数为空,那么相当于我们会监听所有的数据变化。
useEffect(effect, deps?);
useEffect 的使用
作为生命周期使用
useEffect
可以看做 react 中 componentDidMount
,componentDidUpdate
和 componentWillUnmount
这三个生命周期函数的组合。当我们传递的 deps 数组中没有数据时,我们的 useEffect
只会在首次渲染的时候触发,因为任何数值的改变不会触发这个钩子,也就类似于 componentDidMount
这个生命周期,注意他们类似但是不完全相同,componentDidMount
理论上和我们的 useLayoutEffect
钩子完全一致,这个之后会说:
useEffect(()=>{
console.log('这是初始化的hooks')
},[])
之前提到了,副作用函数还可以通过返回一个函数来指定如何清除副作用。如果在上述情况在使用,也就相当于这个返回的函数会在组件卸载前执,也就是相当于 componentWillUnmount
生命周期:
useEffect(()=>{
console.log('这是初始化的hooks')
return ()=>{
console.log('这是卸载的hooks')
}
},[])
当我们设置第二个参数为空时,在初次渲染执行一次后,会监听所有数据的更新,数据更新都会触发useEffect
, 也就是说类似于componentDidMount
、componentDidUpdate
这两个生命周期,但是也有所不同,原理和上面的一致,我们稍后会说::
useEffect(()=>{
console.log('这是更新的hooks')
})
监控数据的变化
如果我们为第二个参数设置了监听的元素,那么在初次渲染执行一次后,只会监听相应元素变化才会触发回调函数。这相当于 vue 的 watch
的用法:
useEffect(()=>{
console.log('num changed')
}, [num])
useLayoutEffect
useLayoutEffect
和 useEffect
的定义完全相同,都是其传入一个副作用函数和一个依赖项数组,他的区别在于:
useEffect
是异步的,useLayoutEffect
是同步的useEffect
的执行时机是浏览器完成渲染之后,而useLayoutEffect
的执行时机是浏览器把内容真正渲染到界面之前
也就说,useLayoutEffect
和我们的 componentDidMount
和 componentDidUpdate
应该是一致的,而 useEffect
在调用时机和处理方式上有所不同,这个我们之后会结合源码来说
比如下面的例子:我们在 useLayoutEffect
和 useEffect
中都设置一个逻辑,让我们的 state 在一秒后从 “hello world” 变成 “world hello”,如果我们使用 useEffect
,因为他的执行时机是浏览器完成渲染后,并且是异步的,所以会先显示"hello world",任何变成 “world hello”;反之 useLayoutEffect
是同步的,并且在渲染之前就执行了,所以我们只会看到 “world hello”
function App() {
const [state, setState] = useState("hello world")
useEffect(() => {
let i = 0;
// 这里是一个 1秒的停顿
setState("world hello");
}, []);
useLayoutEffect(() => {
let i = 0;
// 这里是一个 1秒的停顿
setState("world hello");
}, []);
return (
<>
<div>{state}</div>
</>
);
}
export default App;
useLayoutEffect 和 useEffect 的使用场景
根据上述的描述,useLayoutEffect 和 useEffect 这两个钩子的区别决定了他们有不一样的使用场景,我们简单来描述一下:
- 如果我们的副作用会影响到渲染,也就说可能会让我们页面展示的内容发生变化,那么我们尽量使用
useLayoutEffect
,因为他不会让用户看到脏数据 - 但是如果你的副作用需要很长时间来处理,就需要使用
useEffect
,因为他是异步的,不会阻塞渲染的逻辑,useLayoutEffect
因为是同步的,它消耗的时间会计算在渲染界面展示给用户的时间中
有一个特殊的情况是 SSR 场景,因为 useLayoutEffect
是在浏览器渲染完成之前执行的,所以它是不会在服务端执行的,所以就有可能导致 SSR 渲染出来的内容和实际的首屏内容并不一致,此时会有一个 warning ,所以在 SSR 场景下应该尽量使用 useEffect
,除非你确定它不会影响两次渲染的结果
useEffect 和 useLayoutEffect 的源码
因为这两个钩子的源码及其类似,所以我们就一起讲解了,主要以 useEffect
为主,辅之以讲解 useLayoutEffect
的区别,最后在两个钩子执行时我们再具体分开来将他们的执行过程的差异:
mount 阶段
我们先来看 mount,去掉 dev 部分后,两个 hook 其实就是调用了一个 mountEffectImpl
函数,但是其中有几个参数我们要讲一讲:
我们传入 mountEffectImpl
参数中包含了很多的 来自 ReactFiberFlags
的标志位,这些标志位有以下的作用:
- PassiveEffect 、PassiveStaticEffect 、 UpdateEffect 以及 LayoutStaticEffect 是用于标记副作用类型的,他们会挂载到 fiber 上,这些标记不一定是由 hooks 创建的 ,也可能是由比如 class 组件的生命周期创建的
- HookPassive 和 HookLayout 则是标记 hooks 的,他们会放在我们的 hooks 的数据结构中
function mountEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
return mountEffectImpl(
PassiveEffect | PassiveStaticEffect,
HookPassive,
create,
deps,
);
}
function mountLayoutEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
let fiberFlags: Flags = UpdateEffect;
if (enableSuspenseLayoutEffectSemantics) {
fiberFlags |= LayoutStaticEffect;
}
return mountEffectImpl(fiberFlags, HookLayout, create, deps);
}
我们继续来看这个关键的 mountEffectImpl
函数,他首先把我们的设定的 fiberFlags 设定到了当前的 Fiber 上,后续我们会通过这个标志判定要不要处理 effect。然后使用 pushEffect
,它会先创建一个 effect 数据结构,接着将 effect 添加到函数组件 fiber 的更新队列 updateQueue 之上,最后返回这个创建的 effect 作为 hook.memoizedState。
function mountEffectImpl(fiberFlags, hookFlags, create, deps): void {
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
currentlyRenderingFiber.flags |= fiberFlags;
hook.memoizedState = pushEffect(
HookHasEffect | hookFlags,
create,
undefined,
nextDeps,
);
}
function pushEffect(tag, create, destroy, deps) {
const effect: Effect = {
tag,
create,
destroy,
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;
}
update 阶段
update 阶段和 mount 阶段大同小异,我们直接来看:
首先在 update 阶段,两个 hook 都调用了 updateEffectImpl
这个函数,这个函数中,我们获取了上次的缓存的 effect,拿出本次的 deps 和这次的进行对比,根据对比的情况来加入不同的内容到我们的 Effect
队列中:
- 如果两次的依赖项没有变化,或者依赖项是空,我们本次不会执行这个 hook,推入一个没有
HookHasEffect
标记的 effect ,并且直接退出函数 - 如果有变化,或者
nextDeps
不存在,那么我们先在 fiber 上打标记,然后推入一个包含HookHasEffect
标记的 effect,说明这个 effect 包含副作用需要被执行
function updateEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
return updateEffectImpl(PassiveEffect, HookPassive, create, deps);
}
function updateLayoutEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
return updateEffectImpl(UpdateEffect, HookLayout, create, deps);
}
function updateEffectImpl(fiberFlags, hookFlags, create, deps): void {
const hook = updateWorkInProgressHook(); // 获取当前的hook
const nextDeps = deps === undefined ? null : deps;
let destroy = undefined;
if (currentHook !== null) {
const prevEffect = currentHook.memoizedState; // 上次渲染时使用的hook
destroy = prevEffect.destroy;
if (nextDeps !== null) {
const prevDeps = prevEffect.deps; // 上次渲染时的依赖项
// 判断依赖项是否发生变化
if (areHookInputsEqual(nextDeps, prevDeps)) {
hook.memoizedState = pushEffect(hookFlags, create, destroy, nextDeps);
return;
}
}
// 若nextDeps为null
}
currentlyRenderingFiber.flags |= fiberFlags;
hook.memoizedState = pushEffect(
HookHasEffect | hookFlags,
create,
destroy,
nextDeps,
);
}
Effect 的处理
根据上面的描述,我们知道了,其实 useEffect
做的只是把副作用加入到了我们 Fiber 的 updateQueue
属性上,然后在 fiber 上做了记号,说明这个 fiber 上有副作用,那么这些 effect 的执行是在哪里呢,我们需要把视线回到之前的教程,关于 commit
阶段这篇中,我们提到了 commit 的三个阶段,其中就包含了副作用的处理,当时我们跳过了这个部分,现在我们终于可以把整个坑补上了:
我们直接从 commitRootImpl
函数开始看,将我们之前没讲过的内容全部理一遍:
flushPassiveEffects
首先是开头的部分,我们要调用一次 flushPassiveEffects
,这个函数的作用执行了所有的 hook,关于这个函数我们马上会详细来说。这一步是为了保证在开始我们的 commit 之前,我们没有未执行的的 effect 了 ,其中rootWithPendingPassiveEffects
是一个很重要变量,我们之后会讲,其中我们使用 do… while 的逻辑是因为,在其中的任务被调度的过程中很可能被高优先级的任务打断,我们必须确保运行之前我们的 rootWithPendingPassiveEffects 是空的,才能保证下面的逻辑没用问题:
function commitRootImpl(
root: FiberRoot,
recoverableErrors: null | Array<mixed>,
transitions: Array<Transition> | null,
renderPriorityLevel: EventPriority,
) {
do {
flushPassiveEffects();
} while (rootWithPendingPassiveEffects !== null);
//.....
}
我们来详细看看这个 flushPassiveEffects
函数:它首先获取了 rootWithPendingPassiveEffects
这个全局变量,之后经过一系列的优先级操作后,它调用了 flushPassiveEffectsImpl
,这是它的核心逻辑
我们只看重要的部分,也就是 commitPassiveUnmountEffects
和 commitPassiveMountEffects
两个函数,他们分别执行 useEffect
在上一次执行的销毁函数以及本次的回调函数
export function flushPassiveEffects(): boolean {
if (rootWithPendingPassiveEffects !== null) {
// .... 省略优先级和DEV操作
try {
// 省略
return flushPassiveEffectsImpl();
} finally {
//省略
}
}
return false;
}
function flushPassiveEffectsImpl() {
if (rootWithPendingPassiveEffects === null) {
return false;
}
// 重置这个 rootWithPendingPassiveEffects
rootWithPendingPassiveEffects = null;
// .... 省略优先级和DEV操作
// 调用useEffect在上一次render时的销毁函数;
commitPassiveUnmountEffects(root.current);
// 调用useEffect在本次render时的回调函数;
commitPassiveMountEffects(root, root.current, lanes, transitions);
// ... 省略
return true;
}
我们先完整的看一个部分的执行,commitPassiveUnmountEffects
依次调用了 commitPassiveUnmountEffects_begin
、commitPassiveUnmountEffects_complete
、 commitPassiveUnmountOnFiber
、 commitHookEffectListUnmount
:
我们都明白了,这其实就是我们整个 fiber 树的从上到下的递归过程,和我们的 fiber 树的创建和更新的一样的,相信也不需要我多解释了,就是先向下递归,没有子节点了就遍历兄弟,直到完成整个遍历,这里我们需要处理两种情况:
- 如果是元素有子节点被删除了(具有 ChildDeletion 标记),我们需要调用
commitPassiveUnmountEffectsInsideOfDeletedTree_begin
函数进行处理,其中调用了commitPassiveUnmountInsideDeletedTreeOnFiber
函数进行逻辑的处理,commitPassiveUnmountEffectsInsideOfDeletedTree_complete
函数完成了循环递归遍历,而这个函数调用了commitHookEffectListUnmount
这个函数来处理副作用的卸载 - 与此同时,还有一种情况是没有元素删除,但是我们使用的
useEffect
需要触发,此时我们需要执行对上一次的副作用进行销毁,然后重新挂载,它依次调用了commitPassiveUnmountEffects_begin
、commitPassiveUnmountEffects_complete
、commitPassiveUnmountOnFiber
这个流程,最后还是提交到了commitHookEffectListUnmount
这个函数
上面的讲述告诉了我们, commitHookEffectListUnmount
这个函数在两个情况下会触发,一个是有元素要被删除了,一个是有元素的 useEffect
需要重新挂载(先销毁再创建)他们唯一的区别是 commitHookEffectListUnmount
的入参有所不同
export function commitPassiveUnmountEffects(firstChild: Fiber): void {
nextEffect = firstChild;
commitPassiveUnmountEffects_begin();
}
function commitPassiveUnmountEffects_begin() {
while (nextEffect !== null) {
const fiber = nextEffect;
const child = fiber.child;
// 这部分是对应有子节点被删除的情况
if ((nextEffect.flags & ChildDeletion) !== NoFlags) {
const deletions = fiber.deletions;
if (deletions !== null) {
for (let i = 0; i < deletions.length; i++) {
const fiberToDelete = deletions[i];
nextEffect = fiberToDelete;
commitPassiveUnmountEffectsInsideOfDeletedTree_begin(
fiberToDelete,
fiber,
);
}
// 孩子被删除了,那么他们的指针引用等都要清除
if (deletedTreeCleanUpLevel >= 1) {
const previousFiber = fiber.alternate;
if (previousFiber !== null) {
let detachedChild = previousFiber.child;
if (detachedChild !== null) {
previousFiber.child = null;
do {
const detachedSibling = detachedChild.sibling;
detachedChild.sibling = null;
detachedChild = detachedSibling;
} while (detachedChild !== null);
}
}
}
nextEffect = fiber;
}
}
// 子树还有 effect
if ((fiber.subtreeFlags & PassiveMask) !== NoFlags && child !== null) {
child.return = fiber;
nextEffect = child;
} else {
// 结束遍历
commitPassiveUnmountEffects_complete();
}
}
}
function commitPassiveUnmountEffects_complete() {
while (nextEffect !== null) {
const fiber = nextEffect;
if ((fiber.flags & Passive) !== NoFlags) {
setCurrentDebugFiberInDEV(fiber);
// 真的核心逻辑
commitPassiveUnmountOnFiber(fiber);
resetCurrentDebugFiberInDEV();
}
// 往兄弟节点走
const sibling = fiber.sibling;
if (sibling !== null) {
sibling.return = fiber.return;
nextEffect = sibling;
return;
}
nextEffect = fiber.return;
}
}
function commitPassiveUnmountOnFiber(finishedWork: Fiber): void {
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case SimpleMemoComponent: {
if (
enableProfilerTimer &&
enableProfilerCommitHooks &&
finishedWork.mode & ProfileMode
) {
startPassiveEffectTimer();
// 执行副作用的 destroy
commitHookEffectListUnmount(
HookPassive | HookHasEffect,
finishedWork,
finishedWork.return,
);
recordPassiveEffectDuration(finishedWork);
} else {
commitHookEffectListUnmount(
HookPassive | HookHasEffect,
finishedWork,
finishedWork.return,
);
}
break;
}
}
}
现在我们来看 commitHookEffectListUnmount
这个函数:
这个函数的获取了我们传入的 Fiber 上的 updateQueue
更新列表,如果它的 tag 和我们传入的参数一致(上文说了分为两种情况),我们获取其 destroy
函数,之后把 destroy
函数设置为空,然后执行这个销毁逻辑:
function commitHookEffectListUnmount(
flags: HookFlags,
finishedWork: Fiber,
nearestMountedAncestor: Fiber | null,
) {
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 destroy = effect.destroy;
effect.destroy = undefined;
if (destroy !== undefined) {
//....
safelyCallDestroy(finishedWork, nearestMountedAncestor, destroy);
//....
}
}
effect = effect.next;
} while (effect !== firstEffect);
}
}
function safelyCallDestroy(
current: Fiber,
nearestMountedAncestor: Fiber | null,
destroy: () => void,
) {
try {
destroy();
} catch (error) {
captureCommitPhaseError(current, nearestMountedAncestor, error);
}
}
经过这个流程 ,我们整棵 fiber 树中有 ChildDeletion
和 Passive
标记的 fiber
中的卸载方法就全都执行了。
之后是 commitPassiveMountEffects
的逻辑,它的执行大体一致,我们不再赘述,感兴趣的可以自己去看源码,最后它会来到 commitPassiveMountOnFiber
这个函数中,我们只看 function 组件的逻辑,它调用了 commitHookEffectListMount
这个函数
function commitPassiveMountOnFiber(
finishedRoot: FiberRoot,
finishedWork: Fiber,
committedLanes: Lanes,
committedTransitions: Array<Transition> | null,
): void {
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case SimpleMemoComponent: {
if (
enableProfilerTimer &&
enableProfilerCommitHooks &&
finishedWork.mode & ProfileMode
) {
startPassiveEffectTimer();
try {
commitHookEffectListMount(HookPassive | HookHasEffect, finishedWork);
} finally {
recordPassiveEffectDuration(finishedWork);
}
} else {
commitHookEffectListMount(HookPassive | HookHasEffect, finishedWork);
}
break;
}
}
}
commitHookEffectListMount
主要的工作是获取我们 fiber 上 updateQueue
的 effect,把其 create
函数作为其 destroy
函数,这里我们要讲一讲这个逻辑是什么意思:
如果我们正常写一个 useEffect
的逻辑,那么 useEffect
里面的函数就是它的 create,那么我们执行 create()
之后,得到的就是它的返回值,如果我们没有返回一个销毁副作用的函数,得到的就是 undefined,那么就执行销毁逻辑的时候也就什么也不会执行了,正如我们 mount 我们的 useEffect
的时候,其初始化传入的 destroy 是 undefined 一样;
但是如果我们在 useEffect
中返回了一个销毁函数,那么我们执行 create()
之后,我们返回的值就是这个函数,这个函数就被绑定到 destroy 属性上了,对应的就是在我们的上文的销毁逻辑中执行
所以这个函数的逻辑其实就是执行我们 useEffect
的函数逻辑,然后挂载我们的销毁函数,让下一次执行的时候执行我们的销毁逻辑
function commitHookEffectListMount(flags: HookFlags, finishedWork: Fiber) {
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 create = effect.create;
effect.destroy = create();
// ....
}
effect = effect.next;
} while (effect !== firstEffect);
}
}
// 例子
/*
useEffect(()=>{
console.log('这是初始化的hooks')
return ()=>{
console.log('这是卸载的hooks')
}
},[])
*/
经过上面的步骤,我们的已经清空了我们之前没用执行的 effect,现在我们可以正式开始我们的 commit
阶段了:
往后看代码,在 commitBeforeMutationEffects
阶段之前,我们看到,我们将 fiber 的 subtreeFlags
和 flags
对 PassiveMask
做了一个与运算,也就是在这个地方,我们之前在 fiber 上做的标记发挥了作用,如果你还记得之前的内容,那么你应该知道, PassiveMask
标记的就是 useEffect
相关的副作用,那么这段处理的意思就是,如果我们当前的 fiber 或者孩子上有 useEffect
相关的副作用,我们使用 scheduleCallback
开启一个任务执行 flushPassiveEffects
函数,与此同时它把 rootDoesHavePassiveEffects
这个变量设置为了 true,记住这个点它很重要!
BeforeMutation
我们这里并没有执行 flushPassiveEffects
,而是给他注册了任务,这个任务需要我们的 React 来调度,在整个 commit 过程中,因为其优先级的操作,我们的 flushPassiveEffects
是不会执行的,而它什么时候开始执行我们之后会提到:
if (
(finishedWork.subtreeFlags & PassiveMask) !== NoFlags ||
(finishedWork.flags & PassiveMask) !== NoFlags
) {
if (!rootDoesHavePassiveEffects) {
rootDoesHavePassiveEffects = true;
pendingPassiveEffectsRemainingLanes = remainingLanes;
pendingPassiveTransitions = transitions;
scheduleCallback(NormalSchedulerPriority, () => {
flushPassiveEffects();
return null;
});
}
}
Mutation
上面部分我们暂时放一放,先看之后的 commitMutationEffects
这个阶段,之前我们讲过,这个阶段主要是对 DOM 进行操作,比如插入删除和更新等等,而我们 hooks 的逻辑就在我们对应 FunctionComponent
的逻辑中,我们直接来看 commitMutationEffectsOnFiber
的逻辑(之前的递归 fiber 的过程可以去看之前的文章,有详细讲过)
我们首先看删除的逻辑,在删除的逻辑中,我们函数的调用流程是 recursivelyTraverseMutationEffects
其中对每个有副作用的孩子调用了 commitDeletionEffects
、commitDeletionEffectsOnFiber
,而这个函数对于函数组件的逻辑是:遍历副作用列表,对于带有 HookLayout
标识的副作用,调用了 safelyCallDestroy
函数,这个函数我们的已经提过了,它就是调用了我们对应的副作用的销毁函数:
case FunctionComponent:
case ForwardRef:
case MemoComponent:
case SimpleMemoComponent: {
if (!offscreenSubtreeWasHidden) {
const updateQueue: FunctionComponentUpdateQueue | null = (deletedFiber.updateQueue: any);
if (updateQueue !== null) {
const lastEffect = updateQueue.lastEffect;
if (lastEffect !== null) {
const firstEffect = lastEffect.next;
let effect = firstEffect;
do {
const {destroy, tag} = effect;
if (destroy !== undefined) {
if ((tag & HookInsertion) !== NoHookEffect) {
safelyCallDestroy(
deletedFiber,
nearestMountedAncestor,
destroy,
);
} else if ((tag & HookLayout) !== NoHookEffect) {
//注意看这个 tag 是HookLayout ,这是我们的 useLayoutEffect 的标识
if (enableSchedulingProfiler) {
markComponentLayoutEffectUnmountStarted(deletedFiber);
}
if (
enableProfilerTimer &&
enableProfilerCommitHooks &&
deletedFiber.mode & ProfileMode
) {
startLayoutEffectTimer();
// 调用销毁函数
safelyCallDestroy(
deletedFiber,
nearestMountedAncestor,
destroy,
);
recordLayoutEffectDuration(deletedFiber);
} else {
safelyCallDestroy(
deletedFiber,
nearestMountedAncestor,
destroy,
);
}
if (enableSchedulingProfiler) {
markComponentLayoutEffectUnmountStopped();
}
}
}
effect = effect.next;
} while (effect !== firstEffect);
}
}
}
recursivelyTraverseDeletionEffects(
finishedRoot,
nearestMountedAncestor,
deletedFiber,
);
return;
}
插入的逻辑只会在原生组件和 fiber 根节点上操作,所以我们不需要来看,继续进入更新逻辑:首先明确,我们 useLayoutEffect
的标志是 HookLayout
,我们直接看代码,在源码逻辑中,我们使用了 commitHookEffectListUnmount
这个逻辑函数处理带有 HookLayout
标记的 effect,根据前文提到的,这个函数处理的就是我们函数的 destroy 逻辑,并且这个过程是一个同步执行的的过程。
根据我们分析的删除和更新两个情况的描述,我们已经明确了,在 commitMutationEffects
这个阶段,我们做的事情就是调用我们的 fiber 上 useLayoutEffect
这个钩子的 destroy 逻辑(销毁函数),那么它的 create 逻辑呢?我们继续看
case FunctionComponent:
case ForwardRef:
case MemoComponent:
case SimpleMemoComponent: {
// 删除操作
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
// 插入操作
commitReconciliationEffects(finishedWork);
if (flags & Update) {
// 省略....
if (
enableProfilerTimer &&
enableProfilerCommitHooks &&
finishedWork.mode & ProfileMode
) {
try {
startLayoutEffectTimer();
// 这个是 useLayoutEffect 标识的 effect
commitHookEffectListUnmount(
HookLayout | HookHasEffect,
finishedWork,
finishedWork.return,
);
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
recordLayoutEffectDuration(finishedWork);
} else {
try {
commitHookEffectListUnmount(
HookLayout | HookHasEffect,
finishedWork,
finishedWork.return,
);
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
}
}
return;
}
Layout
最后我们来到 commitLayoutEffects
这个阶段,它执行了 commitHookEffectListMount
这个函数来处理我们的 hooks,我们还是跳过整个 fiber 树的递归过程,直接来看处理函数 commitLayoutEffectOnFiber
,在 FunctionComponent
对于的逻辑中,每个分支都调用了 commitHookEffectListMount
这个函数传入了 HookLayout
这个标识,我们已经直到了这个函数是处理 create 逻辑和挂载 destroy 的
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case SimpleMemoComponent: {
if (
!enableSuspenseLayoutEffectSemantics ||
!offscreenSubtreeWasHidden
) {
if (
enableProfilerTimer &&
enableProfilerCommitHooks &&
finishedWork.mode & ProfileMode
) {
try {
startLayoutEffectTimer();
commitHookEffectListMount(
HookLayout | HookHasEffect,
finishedWork,
);
} finally {
recordLayoutEffectDuration(finishedWork);
}
} else {
commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork);
}
}
break;
}
useLayoutEffect总结
那么到此我们的 useLayoutEffect
函数的逻辑已经执行完毕了,我们总结一下:
-
在
commitMutationEffects
这个阶段,执行我们删除节点和更新节点的销毁逻辑,也就是 destroy 函数 -
在
commitLayoutEffects
这个阶段,执行我们节点的副作用函数 create 逻辑并且挂载我们的 destroy 函数
Layout之后
最后我们继续看我们 commitRoot
的代码,在我们的三个阶段执行完毕之后,有这样一段代码,如果我们的 rootDoesHavePassiveEffects
的 true ,那么我们把我们的 rootWithPendingPassiveEffects
设置成 root,我们回忆一下,rootDoesHavePassiveEffects
是我们在最开始阶段要大家记住的变量,它代表了我们有 useEffect
的hook,而 rootWithPendingPassiveEffects
是一进入我们的逻辑就进行判定的,如果我们的 rootWithPendingPassiveEffects
不是空的就会执行一次我们的 flushPassiveEffects
处理我们的副作用。
与此同时,我们可以观察到,我们的 flushPassiveEffects
中也进行了 rootWithPendingPassiveEffects
的判断,也就是说,只有我们的 rootWithPendingPassiveEffects
不是空才回去执行操作,也就是说,我们的 flushPassiveEffects
也必须等到我们的 commitLayoutEffects
结束之后给予了赋值才能正常执行
if (rootDoesHavePassiveEffects) {
rootDoesHavePassiveEffects = false;
rootWithPendingPassiveEffects = root;
pendingPassiveEffectsLanes = lanes;
} else {
//....
}
/**
do {
flushPassiveEffects();
} while (rootWithPendingPassiveEffects !== null);
**/
/**
export function flushPassiveEffects(): boolean {
if (rootWithPendingPassiveEffects !== null) {
}
return false;
}
**/
useEffect总结
现在我们来总结一下我们的 useEffect
的运行逻辑的:
- 首先我们在
commit
阶段一开始就执行了一次flushPassiveEffects
函数,这个函数遍历了我们的 fiber 上的副作用列表,对每个标记的HookPassive
的副作用(useEffect
创建的)调用它的 destroy 逻辑,然后在调他们的 create 逻辑并且挂载 destroy 函数,这个逻辑中把我们的rootWithPendingPassiveEffects
变量设置为了 null,这一步是因为上一次的渲染可能因为我们开启的flushPassiveEffects
被高优先级的任务抢占或者其他情况而没执行,我们需要确保进入 commit 阶段前我们没有未执行的useEffect
副作用了,这个逻辑也需要通过 do… while… 逻辑保证我们能顺利执行完毕(执行过程中也可能被抢占) - 在进入
commitBeforeMutationEffects
之前,如果显示我们的节点上具有HookPassive
标记(在 render 过程中会做好记号),说明我们有需要处理的useEffect
副作用,我们把rootDoesHavePassiveEffects
这个全局变量设置为 true ,之后我们开启一个调度,给我们的flushPassiveEffects
一个低优先级的任务,然后开始我们的commit
阶段的逻辑 - 在
commit
阶段的三个子阶段执行完毕后,我们根据rootDoesHavePassiveEffects
判断是不是有需要处理的useEffect
副作用, 如果有,那么我们把 FiberRoot 给予我们的rootWithPendingPassiveEffects
变量,这个,之后随着commit
阶段的结束,我们注册的任务开始了调度,此时我们的rootWithPendingPassiveEffects
包含了 effectList,就可以正常执行我们的处理逻辑 - 如果遍历正常结束,那么我们的
rootWithPendingPassiveEffects
会被置为 null,此时我们下一轮调度就不会走开头的 do… while… 逻辑清空rootWithPendingPassiveEffects
,但是如果我们的flushPassiveEffects
运行中被中断了,下一次运行时就仍会存在rootWithPendingPassiveEffects
,此时就需要清空他们了,要注意,如果我们没有后续的需要调度任务了,那么我们的flushPassiveEffects
肯定也不会被打断,就能顺利执行完毕,所以不用担心出现有useEffect
没执行的情况
总结
关于 effect 相关的 hook 终于是讲完了,本来想顺便理一下 class 组件的生命周期的,发现内容有些多就暂时不做了,之后有时间会补上,我们整体来总结一下我们的 effect 的 hook:
useEffect
和useLayoutEffect
是我们最常见的几个 hooks ,给函数组件增加了操作副作用的能力。useEffect
是异步的,useLayoutEffect
是同步的;useEffect
的执行时机是浏览器完成渲染之后,而useLayoutEffect
的执行时机是浏览器把内容真正渲染到界面之前- 他们的创建和更新都依赖于
mountEffectImpl
这个函数,这个函数创建了一个 effect 数据结构,包括其创建、销毁的函数以及标识它的 tag ,这个 tag 让我们区分useEffect
和useLayoutEffect
,之后把它放到了 fiber 的updateQueue 之上 - 他们的更新就是比较了我们传入的 deps 数组,根据他有没有变化来判定我们的 hook 是不是需要调用,之后把 effect 数据结构推入,如果我们不需要调用这个 hook,我们推入的数据结构将不会有一个
HookHasEffect
标记,那么它之后也不会执行 - 副作用相关的 hook 的调用时机在
commit
阶段:useLayoutEffect
在Mutation
这个阶段,执行我们删除节点和更新节点的销毁逻辑,也就是 destroy 函数,在Layout
这个阶段,执行我们节点的副作用函数 create 逻辑并且挂载我们的 destroy 函数useEffect
则是异步调用的,在commit
阶段一开始就会执行一次它的处理,清空遗留下来的 hook;在进入BeforeMutation
之前,我们会开启一个调度,执行我们的useEffect
hook;在commit
阶段的三个子阶段执行完毕后,我们把 effectList 给予rootWithPendingPassiveEffects
这个变量,我们之前注册的任务也得以开始调度,从rootWithPendingPassiveEffects
获取 effectList 然后执行他们的处理
至此我终于把前面所有的坑全部填上了,顺便发现之前的 commit
阶段的讲解有所纰漏,我也已经进行了改正,之后我打算再讲讲一个常用的 useContext
全局配置的相关内容,然后总结我们的整个 React 18 的流程正式结束这个系列教程,也感谢大家一路的阅读