「这是我参与11月更文挑战的第1天,活动详情查看:2021最后一次更文挑战」
react 版本:v17.0.3
1、前言
在 React Hooks 源码解读之Hook入口 一文中,我们介绍了 Hooks 的入口及hook处理函数的挂载,从 hook 处理函数的挂载关系我们可以得到这样的等式:
-
组件挂载阶段:
useEffect = ReactCurrentDispatcher.current.useEffect = HooksDispatcherOnMount.useEffect = mountEffect;
-
组件更新阶段:
useEffect = ReactCurrentDispatcher.current.useEffect = HooksDispatcherOnUpdate.useEffect = updateEffect;
因此,组件在首次加载时,执行 useEffect,其实执行的是 mountEffet,而组件更新时,则执行的是updateEffect 。
2、挂载阶段
组件在挂载阶段,执行 useEffect,实际上执行的是 mountEffect,下面我们来这个函数的实现。
2.1 mountEffect
// packages/react-reconciler/src/ReactFiberHooks.new.js
function mountEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
if (
__DEV__ &&
enableStrictEffects &&
(currentlyRenderingFiber.mode & StrictEffectsMode) !== NoMode
) {
return mountEffectImpl(
// | 位运算符,标识 fiber 节点的副作用异步执行的
MountPassiveDevEffect | PassiveEffect | PassiveStaticEffect,
HookPassive, // 标识 hook 为 useEffect
create, // useEffect 第的第一个参数 callback
deps, // useEffect 的第二个可选参数 依赖项 []
);
} else {
return mountEffectImpl(
// | 位运算符,标识 fiber 节点的副作用
PassiveEffect | PassiveStaticEffect,
HookPassive, // 标识 hook 为 useEffect
create, // useEffect 第的第一个参数 callback
deps, // useEffect 的第二个可选参数 依赖项 []
);
}
}
可以看到,mountEffect 接受 useEffect 传入的 回调函数(create) 和 依赖项{deps) 两个参数,并返回了 mountEffectImpl 的执行结果。在调用 mountEffectImpl 时传入了用于位运算的 fiber 节点标识和 hook 对象的标识,还传入了 useEffect提供的两个参数 callback 和依赖项。
在 mountEffect 函数中将 mountEffectImpl 的执行结果返回给了 useEffect,我们来看看 mountEffectImpl 做了什么事情。
2.2 mountEffectImpl
// packages/react-reconciler/src/ReactFiberHooks.new.js
function mountEffectImpl(fiberFlags, hookFlags, create, deps): void {
// 创建 hook 对象,将 hook 对象添加到 workInProgressHook 单向链表中,返回最新的 hook 链表
const hook = mountWorkInProgressHook();
// 初始化 useEffect 的第二个参数 依赖项
const nextDeps = deps === undefined ? null : deps;
// 当前 fiber 节点的二进制值,区分当前 effect 是 useEffect 还是 useLayoutEffect
currentlyRenderingFiber.flags |= fiberFlags;
// 初始化 effect 链表,添加到 useEffect hook 的 memoizedState 属性上
// 因此 useEffect hook 的 memoizedState 并不是一个具体的值(useState、useReducer 的 memoizedState 是一个具体的值),而是一个 effect 链表
hook.memoizedState = pushEffect(
// HookHasEffect 和 hookFlags 做位运算
// HookHasEffect 标记Effect的回调和销毁函数需要执行
// hookFlags 参数值为 HookPassive,表示 hook 是 useEffect
HookHasEffect | hookFlags,
create, // useEffect hook 的第一个参数 callback
undefined,
nextDeps, // useEffect hook 的第二个参数 依赖项数组
);
}
可以看到,在 mountEffectImpl 里,首先调用 mountWorkInProgressHook(),将当前的 hook 添加到 workInProgressHook 单向链表中,并返回最新的 hook 链表:
const hook = mountWorkInProgressHook();
接着初始化 useEffect 的第二个参数 依赖项数组:
const nextDeps = deps === undefined ? null : deps;
再接下来将标识fiber节点的二进制值添加到当前fiber 节点的 flags 属性上:
currentlyRenderingFiber.flags |= fiberFlags;
最后,调用 pushEffect() ,初始化 effect 链表,将其添加到 useEffect hook 的 memoizedState 属性上:
hook.memoizedState = pushEffect(
// HookHasEffect 和 hookFlags 做位运算
// HookHasEffect 标记Effect的回调和销毁函数需要执行
// hookFlags 参数值为 HookPassive,表示 hook 是 useEffect
HookHasEffect | hookFlags,
create, // useEffect hook 的第一个参数 callback
undefined,
nextDeps, // useEffect hook 的第二个参数 依赖项数组
);
由此可知道, useEffect 的 memoizedState 并不是一个具体的值,而是一个 effect 链表。而 useState、useReducer 的 memoizedState 是一个具体的值。
2.3 mountWorkInProgressHook
在 mountEffectImpl() 函数中,使用 mountWorkInProgressHook() 函数创建了一个新的 hook 对象,我们来看看它是如何被创建的:
// packages/react-reconciler/src/ReactFiberHooks.new.js
// 创建一个新的 hook 对象,并返回当前的 workInProgressHook 对象
// workInProgressHook 对象是全局对象,在 mountWorkInProgressHook 中首次初始化
function mountWorkInProgressHook(): Hook {
const hook: Hook = {
memoizedState: null,
baseState: null,
baseQueue: null,
queue: null,
next: null,
};
// Hooks are stored as a linked list on the fiber's memoizedState field
// 将 新建的 hook 对象以链表的形式存储在当前的 fiber 节点memoizedState属性上
// 只有在第一次打开页面的时候,workInProgressHook 为空
if (workInProgressHook === null) {
// This is the first hook in the list
// 链表上的第一个 hook
// currentlyRenderingFiber: The work-in-progress fiber. I've named it differently to distinguish it fromthe work-in-progress hook.
currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
} else {
// Append to the end of the list
// 已经存在 workInProgressHook 对象,则将新创建的这个 Hook 接在 workInProgressHook 的尾部,形成链表
workInProgressHook = workInProgressHook.next = hook;
}
return workInProgressHook;
}
可以看到,在新建一个 hook 对象时,如果全局的 workInProgressHook 对象不存在 (值为 null),即组件在首次渲染时,将新建的 hook 对象赋值给 workInProgressHook 对象,同时将 hook 对象赋值给 currentlyRenderingFiber 的 memoizedState 属性,如果 workInProgressHook 对象已经存在,则将 hook 对象接在 workInProgressHook 的尾部,从而形成一个单向链表。
在 mountEffect 的最后,调用 pushEffect 初始化了hook 对象上的 effect 链表,并添加到 useEffect hook 的 memoizedState 属性上。接下来我们来看看 pushEffect 的实现。
2.4 pushEffect
// packages/react-reconciler/src/ReactFiberHooks.new.js
function pushEffect(tag, create, destroy, deps) {
// 新建一个 effect 对象
const effect: Effect = {
tag, // effect的tag,是一个二进制值,用于区分useEffect和useLayoutEffect
create, // useEffect 的第一个参数 callback
destroy,
deps, // useEffect 的 第二个参数 依赖项数组
// Circular
next: (null: any), // 链表的next指针,链接下一个 effect
};
// 从当前 Fiber 节点的 updateQueue 属性上获取当前 Fiber 节点的 更新队列
let componentUpdateQueue: null | FunctionComponentUpdateQueue = (currentlyRenderingFiber.updateQueue: any);
if (componentUpdateQueue === null) {
// 如果当前 Fiber 节点的更新队列不存在,则创建一个更新队列
componentUpdateQueue = createFunctionComponentUpdateQueue();
currentlyRenderingFiber.updateQueue = (componentUpdateQueue: any);
// 将 effect 链表添加到 更新队列上
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
// 当前 Fiber 节点上以存在更新队列,将当前的 effect 添加到 effect 链表的末尾
// effect 是一个环形链表
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;
}
}
// 返回 effect 环形链表
return effect;
}
首先,根据传进来的参数,创建一个 effect 对象,该 effect 对象上存储着 useEffect 的两个参数: callback 和 依赖项数组,还存储着用于标识 hook 对象的二进制数值,每个 hook 对象都是通过 next指针连接下一个 effect。
const effect: Effect = {
tag, // 标识是useEffect还是useLayoutEffect(HasEffect、Layout、Passive )
create, // useEffect 的第一个参数 callback
destroy,
deps, // useEffect 的 第二个参数 依赖项数组
// Circular
next: (null: any), // 链表的next指针,链接下一个 effect
};
然后判断当前 Fiber 节点是否已经存在更新队列,如果不存在,则新建一个更新队列,并将当前的 effect 添加到更新队列的 lastEffect 属性上,并将 effect 的 next 指向自己,形成环形链表。
if (componentUpdateQueue === null) {
// 如果当前 Fiber 节点的更新队列不存在,则创建一个更新队列
componentUpdateQueue = createFunctionComponentUpdateQueue();
currentlyRenderingFiber.updateQueue = (componentUpdateQueue: any);
// 将 effect 链表添加到 更新队列上
componentUpdateQueue.lastEffect = effect.next = effect;
}
如若更新队列已经存在,则将当前的effect添加到 effect 链表的末尾,并将当前的effect的next指向第一个effect,形成环形链表。
else {
// 当前 Fiber 节点上以存在更新队列,将当前的 effect 添加到 effect 链表的末尾
// effect 是一个环形链表
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;
}
}
最后是将 effect 返回给 mountEffectImpl,在 mountEffectImpl 中将 effect 赋值给 useEffect hook 的 memoizedState 属性。
这就是组件首次渲染时,useEffect 创建 effect 的整个过程,我们看下它的流程图:
3、更新阶段
接下来我们来看看更新过程中 useEffect 实际调用的方法 updateEffect。
3.1 updateEffect
// packages/react-reconciler/src/ReactFiberHooks.new.js
function updateEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
// 删除了 dev 代码
// PassiveEffect: 二进制值,标识 fiber 节点的副作用是异步执行的(即使用的是useEffect,useEffect 是异步的)
// HookPassive: 表示 hook 为 useEffect
// create: useEffect 第的第一个参数 callback
// deps: useEffect 的第二个可选参数 依赖项 []
// 比较前后的 deps ,判断是否需要重新执行useEffect的第一个参数 callback
return updateEffectImpl(PassiveEffect, HookPassive, create, deps);
}
可以看到,updateEffect 接受 useEffect 传入的 回调函数(create) 和 依赖项{deps) 两个参数,并返回了 updateEffectImpl 的执行结果。updateEffectImpl 通过比较前后的 deps (useEffect 的第二个参数) ,判断是否需要重新执行 useEffect 的 callback 参数,我们来看看 updateEffectImpl 的实现。
3.2 updateEffectImpl
// packages/react-reconciler/src/ReactFiberHooks.new.js
function updateEffectImpl(fiberFlags, hookFlags, create, deps): void {
// 当前正在更新的 fiber 节点上的 hook
const hook = updateWorkInProgressHook();
// 新的 deps
const nextDeps = deps === undefined ? null : deps;
let destroy = undefined;
//currentHook: 当前 fiber 节点上的 hook 对象
// 当前 fiber 节点上存在 hook 对象
if (currentHook !== null) {
// 获取旧的 effect 状态
const prevEffect = currentHook.memoizedState;
destroy = prevEffect.destroy;
// 如果新的 deps 存在
if (nextDeps !== null) {
// 获取旧的 deps
const prevDeps = prevEffect.deps;
// 比较新旧 deps 是否相同
if (areHookInputsEqual(nextDeps, prevDeps)) {
// 新旧 deps 相同,传入 hookFlags, 表示不需要 update,更新 hook 对象上的 effect 链
hook.memoizedState = pushEffect(hookFlags, create, destroy, nextDeps);
return;
}
}
}
// 代码执行到这里,表示新旧的 deps 不一样
// 更新 当前 fiber 节点上的 flags 标识,区分当前 effect 是 useEffect 还是 useLayoutEffect
currentlyRenderingFiber.flags |= fiberFlags;
// 传入 HookHasEffect | hookFlags 位运算的结果,更新 hook 对象上的effect 链
hook.memoizedState = pushEffect(
// HookHasEffect 和 hookFlags 做位运算
// HookHasEffect 标记Effect的回调和销毁函数需要执行
// hookFlags 参数值为 HookPassive,表示 hook 是 useEffect
HookHasEffect | hookFlags,
create,
destroy,
nextDeps,
);
}
首先调用 updateWorkInProgressHook() ,获取当前正在执行 update 任务的fiber节点上的hook 对象:
const hook = updateWorkInProgressHook();
接着获取新的 deps,方便与旧的 deps 比较,从而决定是否需要更新:
const nextDeps = deps === undefined ? null : deps;
然后从当前的 hook 对象上获取旧的 deps,调用 areHookInputsEqual() 来比较前后 deps 是否相同:
// 当前 fiber 节点上存在 hook 对象
if (currentHook !== null) {
// 获取旧的 effect 状态
const prevEffect = currentHook.memoizedState;
destroy = prevEffect.destroy;
// 如果新的 deps 存在
if (nextDeps !== null) {
// 获取旧的 deps
const prevDeps = prevEffect.deps;
// 比较新旧 deps 是否相同
if (areHookInputsEqual(nextDeps, prevDeps)) {
// 新旧 deps 相同,传入 hookFlags, 表示不需要 update,更新 hook 对象上的 effect 链
hook.memoizedState = pushEffect(hookFlags, create, destroy, nextDeps);
return;
}
}
}
如果 areHookInputsEqual() 返回的结果为 true,说明前后的 deps 是一样的,该 effect 没有产生副作用,则调用 pushEffect,传入hookFlags,表示不更新执行 useEffect 的callback,然后更新 effect 链。
如果 areHookInputsEqual() 返回的结果为 false,则会执行下面的语句:
// 更新 当前 fiber 节点上的 flags 标识
currentlyRenderingFiber.flags |= fiberFlags;
// 传入 HookHasEffect | hookFlags 位运算的结果,更新 hook 对象上的effect 链
hook.memoizedState = pushEffect(
// HookHasEffect 和 hookFlags 做位运算
// HookHasEffect 标记Effect的回调和销毁函数需要执行
// hookFlags 参数值为 HookPassive,表示 hook 是 useEffect
HookHasEffect | hookFlags,
create,
destroy,
nextDeps,
);
可见,updateEffectImpl 最主要的作用,就是通过比较前后的 deps,判断是否是需要重新执行 useEffect 的 callback。
3.3 updateWorkInProgressHook
在 updateEffectImpl() 函数中,通过 updateWorkInProgressHook() 函数获取到了当前正在工作中的 Hook,即 workInProgressHook,我们来看看 updateWorkInProgressHook 的实现:
// packages/react-reconciler/src/ReactFiberHooks.new.js
function updateWorkInProgressHook(): Hook {
// This function is used both for updates and for re-renders triggered by a
// render phase update. It assumes there is either a current hook we can
// clone, or a work-in-progress hook from a previous render pass that we can
// use as a base. When we reach the end of the base list, we must switch to
// the dispatcher used for mounts.
// 获取 当前 hook 的下一个 hook
let nextCurrentHook: null | Hook;
if (currentHook === null) {
const current = currentlyRenderingFiber.alternate;
if (current !== null) {
nextCurrentHook = current.memoizedState;
} else {
nextCurrentHook = null;
}
} else {
nextCurrentHook = currentHook.next;
}
// 取下一个 hook 为当前的hook
let nextWorkInProgressHook: null | Hook;
if (workInProgressHook === null) {
nextWorkInProgressHook = currentlyRenderingFiber.memoizedState;
} else {
nextWorkInProgressHook = workInProgressHook.next;
}
if (nextWorkInProgressHook !== null) {
// There's already a work-in-progress. Reuse it.
workInProgressHook = nextWorkInProgressHook;
nextWorkInProgressHook = workInProgressHook.next;
currentHook = nextCurrentHook;
} else {
// Clone from the current hook.
// 拷贝当前的 hook,作为当前正在工作中的 workInProgressHook
invariant(
nextCurrentHook !== null,
'Rendered more hooks than during the previous render.',
);
currentHook = nextCurrentHook;
const newHook: Hook = {
memoizedState: currentHook.memoizedState,
baseState: currentHook.baseState,
baseQueue: currentHook.baseQueue,
queue: currentHook.queue,
next: null,
};
if (workInProgressHook === null) {
// This is the first hook in the list.
currentlyRenderingFiber.memoizedState = workInProgressHook = newHook;
} else {
// Append to the end of the list.
workInProgressHook = workInProgressHook.next = newHook;
}
}
return workInProgressHook;
}
这里分两种情况:
- 如果是在 render 阶段,则会取下一个 hook 作为当前的hook,并返回 workInProgressHook;
- 如果是在 re-render 阶段,则在当前处理周期中,继续取当前的 workInProgressHook 做更新处理,最后再返回 workInProgressHook。
前后的 deps 是否一样,是通过 areHookInputsEqual() 来判断的,我们来看看它的实现。
3.4 areHookInputsEqual
// packages/react-reconciler/src/ReactFiberHooks.new.js
function areHookInputsEqual(
nextDeps: Array<mixed>,
prevDeps: Array<mixed> | null,
) {
// 删除了 dev 代码
if (prevDeps === null) {
// 删除了 dev 代码
return false;
}
// 删除了 dev 代码
// deps 是一个 Array,循环遍历去比较 array 中的每个 item
for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
// is比较函数是浅比较
if (is(nextDeps[i], prevDeps[i])) {
continue;
}
return false;
}
return true;
}
在 areHookInputsEqual() 函数中,循环遍历 deps ,调用 is 方法去比较依赖项数组中的每个依赖,值得注意的是,is 方法是浅比较,也就是说如果是深比较那一定会更新的。下面是 is 方法的源码:
// packages/shared/objectIs.js
function is(x: any, y: any) {
return (
(x === y && (x !== 0 || 1 / x === 1 / y)) || (x !== x && y !== y) // eslint-disable-line no-self-compare
);
}
const objectIs: (x: any, y: any) => boolean =
typeof Object.is === 'function' ? Object.is : is;
export default objectIs;
若当前浏览器支持 Object.is() 方法,则调用该方法来判断两个值是否相同,若不支持,则调用React自己实现 is 方法来比较。
3.5 pushEffect
无论是否需要重新执行 useEffect 的 callback,最后都会调用 pushEffect 去更新 hook 对象上的 effect 链表,然后将更新后的 effect 添加到 hook 对象上的 memoizedState 属性上。pushEffect 在 「3.4 pushEffect」小节已经讲解过,这里不再赘述。