最近在学习慕课网 手写 React 高质量源码迈向高阶开发,之前自己也尝试看过源码,不过最终放弃了
放弃的最主要原因是 react 内部的调用链太长了,每天在缕清调用链上都花了不少时间,createRoot 都没有看完
最近看到慕课网有一个 react 源码课,就想着跟着课程然后在自己源码,看看这次能够看到什么地步
它这个课程前八章 是 react@16 的源码,从第九章开始才是 react@18 的源码
从本章开始学习的是 react@18 的源码,从 fiber 开始,涉及 beginWork,completeWork,commitWork,调度策略,优先级等
React 源码系列:
react@16- 第 1 篇:createElement 和 render 函数实现原理
- 第 2 篇:函数组件和类组件及 ref 和 setState 的实现
- 第 3 篇:优化渲染过程之 dom diff
- 第 4 篇:类组件增强------生命周期函数
- 第 5 篇:性能优化------PureComponent 和 memo
- 第 6 篇:hooks------useEffect、useState、useMemo 等源码实现
react@18- 第 1 篇:beginWork 前的准备工作:jsxDEV,createRoot,render 源码实现
- 第 2 篇:beginWork 工作原理
- 第 3 篇:4 张图带你看懂 beginWork 和 completeWork 工作过程
- 第 4 篇:一张图看懂 React 合成事件原理
- 第 5 篇:commitWork 工作原理
- 第 6 篇:Fiber 架构的 DOM Diff 原理
- 第 7 篇:图解 useState 原理
- 第 8 篇:
useEffect和useLayoutEffect源码实现
我们在上一篇中实现了 useState 和 useReducer
这一篇来实现 useEffect
它也是和 useState 一样,定义在 react 包中
js
// react/src/ReactHooks.js
function useEffect(create, deps) {
const dispatcher = resolveDispatcher();
return dispatcher.useEffect(create, deps);
}
然后通过 react/index.js 暴露给开发者使用,__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED 给内部包使用
在 ReactFiberHooks 文件中分别给 HooksDispatcherOnMount 和 HooksDispatcherOnUpdate 添加 useEffect 方法
js
// 函数组件挂载时执行的 hooks
const HooksDispatcherOnMount = {
useReducer: mountReducer,
useState: mountState,
useEffect: mountEffect,
};
// 函数组件更新时执行的 hooks
const HooksDispatcherOnUpdate = {
useReducer: updateReducer,
useState: updateState,
useEffect: updateEffect,
};
这两个对象实在 renderWithHooks 中调用的,而 renderWithHooks 又是在 beginWork 阶段调用的,所以也就是说 useEffect 初次挂载和更新都是在 beginWork 时执行
mountEffect
mountEffect 方法初始渲染时调用的
第一个参数是 useEffect 的函数,第一个参数是 useEffect 的依赖,内部调用了 mountEffectImpl 方法,传入的两个副作用的标记 PassiveEffect 和 HookPassive
PassiveEffect 和 HookPassive 都是 Passive,这里是给它重命名了一下
他们的区别是:
PassiveEffect是一个FiberFlags表示当前的Fiber有一个副作用HookPassive是一个HookFlags表示当前的Hook有一个副作用
他们使用要到 commitWork 阶段,所以这里暂时先不用管
js
// react-reconciler/src/ReactFiberHooks.js
function mountEffect(create, deps) {
return mountEffectImpl(PassiveEffect, HookPassive, create, deps);
}
mountEffectImpl
这个函数主要做了这种;
- 为当前的
useEffect初始化一个hook- 通过
mountWorkInProgressHook初始化,这个函数我们在useReducer中已经讲过了,它的作用就是初始化一个hook等待更新时使用 - 这个
hook是保存在currentRenderingFiber.memoizedState中的,从当前的Fiber中的第一个hook依次向下通过next的链接在一起
- 通过
- 将当前的
Fiber.flags设置为Passive - 将刚刚初始化的
hook.memoizedState挂载一个副作用函数effectuseReducer的memoizedState中挂载的是useReducer的初始值
js
// react-reconciler/src/ReactFiberHooks.js
function mountEffectImpl(fiberFlags, hookFlags, create, deps) {
// 创建一个 hook 这个 hook 会关联在 currentlyRenderingFiber.memoizedState 上
// currentyRenderingFiber 是从函数组件中编写的的第一个 hook 开始一次将里面的 hook 用 next 的方式串联起来
const hook = mountWorkInProgressHook();
// useEffect 的依赖
const nextDeps = deps === undefined ? null : deps;
// 将当前的 Fiber 标记为 PassiveEffect
currentlyRenderingFiber.flags |= fiberFlags;
// 当前的 hook.memoizedState 是一个 effect 函数
hook.memoizedState = pushEffect(HookHasEffect | hookFlags, create, undefined, nextDeps);
}
pushEffect
pushEffect 作用是创建一个 effect 对象,并将 useEffect 之间形成链表,通过 lastEffect 链接
如果当前的 Fiber 上不存在 updateQueue,说明现在处理的是第一个 useEffect,就初始化一个 updateQueue,并将创建的 effect 放入 updateQueue 中
如果当前的 Fiber 上存在 updateQueue,说明现在处理的 hook 不是 Fiber 中的第一个 hook,由于 pushEffect 只处理 useEffect,就有可能没有 lastEffect
没有 lastEffect,就将 effect 赋值 lastEffect,如果有的话就将 useEffect 单独做成一个链表,lastEffect 是指向最后一个 useEffect 的 effect
js
// react-reconciler/src/ReactFiberHooks.js
function pushEffect(tag, create, destroy, deps) {
const effect = { tag, create, destroy, deps, next: null };
// 如果当前的 Fiber 上不存在 updateQueue,说明当前处理的是 Fiber 的第一个 hook 是 useEffect
let componentUpdateQueue = currentlyRenderingFiber.updateQueue;
// 如果不存在 updateQueue,就初始化一个 updateQueue
if (componentUpdateQueue === null) {
componentUpdateQueue = createFunctionComponentUpdateQueue();
currentlyRenderingFiber.updateQueue = componentUpdateQueue;
// lastEffect 指向 Fiber 中最后一个 useEffect
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
// 如果存在 updateQueue,说明当前处理的不是 Fiber 的第一个 hook
const lastEffect = componentUpdateQueue.lastEffect;
// 如果不存在 lastEffect,说明当前处理的的是 Fiber 中的第一个 useEffect
if (lastEffect === null) {
// lastEffect 指向 Fiber 中最后一个 useEffect
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
// 如果存在 lastEffect,说明当前处理的是 Fiber 中的第二个及之后的 useEffect
const firstEffect = lastEffect.next;
lastEffect.next = effect;
effect.next = firstEffect;
// lastEffect 指向 Fiber 中最后一个 useEffect
componentUpdateQueue.lastEffect = effect;
}
}
return effect;
}
function createFunctionComponentUpdateQueue() {
return { lastEffect: null };
}
updateEffect
updateEffect 方法是在更新时调用的,它的参数和 mountEffect 一样
js
// react-reconciler/src/ReactFiberHooks.js
function updateEffect(create, deps) {
return updateEffectImpl(PassiveEffect, HookPassive, create, deps);
}
至于什么时候调用 mountEffect 和 updateEffect,是在 renderWithHooks 中决定的
js
// react-reconciler/src/ReactFiberHooks.js
function renderWithHooks(current, workInProgress, Component, props) {
// 这一步骤其实很关键,在更新时如果 updateQueue 没有设置为 null
// 那么更新时的 effect 会和初次渲染的 effect 组成链表,这是有问题的
// 所有 react 在 renderWithHooks 执行时将 updateQueue 设置为 null
workInProgress.updateQueue = null;
// 如果 current 有值,说明是更新,否则是初次渲染
// 当然,如果没有要更新的 state,也不用走更新逻辑
if (current !== null && current.memoizedState !== null) {
// 更新,setXXX 时,给 useReducer, useEffect 等 hook 赋值
ReactCurrentDispatcher.current = HooksDispatcherOnUpdate;
} else {
// 初始渲染,给 useReducer, useEffect 等 hook 赋值
ReactCurrentDispatcher.current = HooksDispatcherOnMount;
}
}
updateEffectImpl
updateWorkInProgressHook 我们在 useState 文章中讲到过,在一个组件中更新时,每一个 hook 都会运行一遍 updateEffectImpl
因为 hook 是由链表串联起来的,所以这里就用 updateWorkInProgressHook 取出当前正在运行的 hook
具体来说,你在一个组件中写了一个 useState,一个 useEffect,可以通过 updateWorkInProgressHook 函数找到 useState 或者 useEffect 对应的 hook
js
const [number, setNumber] = useState(1);
useEffect(() => {
console.log("useEffect");
}, []);
其实这里 hook 的值是从 currentHook 中拷贝过来的,但不是同一个对象
如果当前的 useEffect 依赖有变化,就将当前的 Fiber 标记一个 PassiveEffect 的 Flags,同时将 effect 中的 tag 标记为 HookHasEffect
也就是说在更新时需要比较 nextDeps 和 prevDeps 是否相同
相同时:
- 当前的
Fiber.flags不标记 - 将当前处理的
useEffect的effect.tag为HookPassive
不同时:
- 将当前的
Fiber.flags标记为PassiveEffect - 将当前处理的
useEffect的effect.tag标记为HookHasEffect | HookPassive
js
// react-reconciler/src/ReactFiberHooks.js
function updateEffectImpl(fiberFlags, hookFlags, create, deps) {
// 取出当前 Fiber 中正在运行的 hook
// 有一个 hook,这个函数就会运行一次,运行之后这个 hook 就是当前需要处理的 hook
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
let destroy;
// 老 hook 存在
if (currentHook !== null) {
const prevEffect = currentHook.memoizedState;
// 拿到上一个 hook 的 effect
destroy = prevEffect.destroy;
// 有依赖
if (nextDeps !== null) {
const prevDeps = prevEffect.deps;
// 依赖相同的情况
if (areHookInputsEqual(nextDeps, prevDeps)) {
// 给 effect tag 标记为 HookHasEffect | HookPassive
hook.memoizedState = pushEffect(hookFlags, create, destroy, nextDeps);
return;
}
}
}
// 依赖不相同的情况,说明当前的 Fiber 需要处理副作用
currentlyRenderingFiber.flags |= fiberFlags;
// 给 effect tag 标记为 HookHasEffect | HookPassive
hook.memoizedState = pushEffect(HookHasEffect | hookFlags, create, destroy, nextDeps);
}
使用 Object.is 比较两个依赖是否相同
js
// react-reconciler/src/ReactFiberHooks.js
function areHookInputsEqual(nextDeps, prevDeps) {
if (prevDeps === null) return null;
for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
// 是同一个对象
if (Object.is(nextDeps[i], prevDeps[i])) continue;
return false;
}
return true;
}
commitWork
beginWork 阶段主要做的事情是将 useEffect 的 effect 函数和依赖保存起来
保存之后,在什么时候使用呢?
那肯定是在 commitWork 阶段使用了
commitWork 的入口函数是 commitRoot
我们检查 root 节点上有没有 Passive 这个标记,如果有存在的话,就执行 scheduleCallback
js
// react-reconciler/src/ReactFiberWorkLoop.js
function commitRoot(root) {
const { finishedWork } = root;
if ((finishedWork.subtreeFlags & Passive) !== NoFlags || (finishedWork.flags & Passive) !== NoFlags) {
// 如果根节点上没有 Passive 这个标记,就将根节点标记为 Passive
// 这么设置的原因是防止在同一个 useEffect 中多次执行 commitWork
if (!rootDoseHavePassiveEffect) {
rootDoseHavePassiveEffect = true;
scheduleCallback(flushPassiveEffects);
}
}
}
设置了之后就需要释放,释放是要等到 commitWork 执行结束之后
js
// react-reconciler/src/ReactFiberWorkLoop.js
function commitRoot(root) {
const subtreeHasEffects = (finishedWork.subtreeFlags & MutationMask) !== NoFlags;
// 查看 RootFiber 是否有处理
const rootHasEffect = (finishedWork.flags & MutationMask) !== NoFlags;
if (subtreeHasEffects || rootHasEffect) {
// commitWork
commitMutationEffectsOnFiber(finishedWork, root);
// 释放 rootDoseHavePassiveEffect
if (rootDoseHavePassiveEffect) {
rootDoseHavePassiveEffect = false;
rootWithPendingPassiveEffects = root;
}
}
}
flushPassiveEffects
flushPassiveEffects 函数的主要逻辑就是执行 useEffect 的 effect 函数和 destroy 函数
js
// react-reconciler/src/ReactFiberWorkLoop.js
function flushPassiveEffects() {
if (rootWithPendingPassiveEffects !== null) {
const root = rootWithPendingPassiveEffects;
// 先执行 useEffect 的 destroy 函数
commitPassiveUnmountEffects(root.current);
// 再执行 useEffect 的 effect 函数
commitPassiveMountEffects(root, root.current);
}
}
commitPassiveMountEffects
commitPassiveMountEffects 函数是 useEffect 的 effect 函数,内部直接调用了 commitPassiveMountOnFiber
js
// react-reconciler/src/ReactFiberCommitWork.js
function commitPassiveMountEffects(root, finishedWork) {
commitPassiveMountOnFiber(root, finishedWork);
}
commitPassiveMountOnFiber 函数的作用是根据 tag 做不同的处理
因为 useEffect 只会出现在函数组件中,也就是说 tag 是 FunctionComponent
tag 是其他值的话,继续往下遍历,与 recursivelyTraversePassiveMountEffects 组成递归
如果是函数组件的话,就检查当前的 Fiber 是否有 Passive 标记,如果有的话就执行 commitHookPassiveMountEffects 函数
也就说 useEffect 是深度优先函数,叶子节点的 useEffect 会先执行
js
// react-reconciler/src/ReactFiberCommitWork.js
function commitPassiveMountOnFiber(finishedRoot, finishedWork) {
const { flags } = finishedWork;
switch (finishedWork.tag) {
case HostRoot: {
recursivelyTraversePassiveMountEffects(finishedRoot, finishedWork);
break;
}
case FunctionComponent: {
// 深度优先
// 因为 recursivelyTraversePassiveMountEffects 内部会检查当前 Fiber 的子节点,如果有子节点又会继续调用 commitPassiveMountOnFiber,形成深度优先
recursivelyTraversePassiveMountEffects(finishedRoot, finishedWork);
// 当前 Fiber 有 Passive 标记
if (flags & Passive) {
// 处理当前 Fiber 的 effect 函数
commitHookPassiveMountEffects(finishedWork, HookHasEffect | HookPassive);
}
break;
}
default: {
recursivelyTraversePassiveMountEffects(finishedRoot, finishedWork);
break;
}
}
}
recursivelyTraversePassiveMountEffects 函数检查当前 Fiber 是否有兄弟节点,如果有兄弟节点的话就依此调用 commitPassiveMountOnFiber
js
// react-reconciler/src/ReactFiberCommitWork.js
function recursivelyTraversePassiveMountEffects(root, parentFiber) {
if (parentFiber.subtreeFlags & Passive) {
let child = parentFiber.child;
while (child !== null) {
commitPassiveMountOnFiber(root, child);
child = child.sibling;
}
}
}
commitHookPassiveMountEffects
commitHookPassiveMountEffects 函数转发到 commitHookEffectListMount
js
// react-reconciler/src/ReactFiberCommitWork.js
function commitHookPassiveMountEffects(finishedWork, hookFlags) {
commitHookEffectListMount(hookFlags, finishedWork);
}
useEffect 的 effect 函数主要就是由 commitHookEffectListMount 函数处理
从当前 Fiber 中取出 updateQueue,因为在 beginWork 阶段时,一个函数组件所有 useEffect 都会放入 updateQueue 中形成链表
这里就是取出 effect 链表,挨个执行 effect 函数
js
// react-reconciler/src/ReactFiberCommitWork.js
function commitHookEffectListMount(flags, finishedWork) {
const updateQueue = finishedWork.updateQueue;
// 取出 updateQueue 中保存的 effect 链表
let lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
if (lastEffect !== null) {
const firstEffect = lastEffect.next;
// 第一个 effect 函数
let effect = firstEffect;
do {
// 如果 effect.tag 是 HookHasEffect | HookPassive 标记就执行 effect.create 函数,并将返回的函数保存到 destroy 中,等待 Unmount 时执行
if ((effect.tag & flags) === flags) {
const create = effect.create;
// 执行 create 函数,得到 destroy 函数
// 将 destroy 函数保存到 effect.destroy 中
effect.destroy = create();
}
effect = effect.next;
} while (effect !== firstEffect);
}
}
commitPassiveUnmountEffects
commitPassiveUnmountEffects 的逻辑和 commitPassiveMountEffects 一样,只是调用的函数不一样
这里需要知道的一点是,整个 Fiber 树的 destroy 函数执行完之后,才会执行 effect 函数
destroy 执行也是遵循深度优先的策略,叶子节点的 destroy 会先执行
js
// react-reconciler/src/ReactFiberCommitWork.js
function commitPassiveUnmountEffects(finishedWork) {
commitPassiveUnmountOnFiber(finishedWork);
}
最后会调用 commitHookEffectListUnmount 函数,执行 useEffect 的 destroy 函数
js
function commitHookEffectListUnmount(flags, finishedWork) {
const updateQueue = finishedWork.updateQueue;
let 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;
// 执行 destroy 函数
if (destroy !== undefined) {
destroy();
}
}
effect = effect.next;
} while (effect !== firstEffect);
}
}
useLayoutEffect
useLayoutEffect 和 useEffect 特别像,它们的区别是 useEffect 是在渲染后执行,useLayoutEffect 是在渲染时执行的
useEffect 是异步执行的,useLayoutEffect 是同步执行
commitLayoutEffects 是 useLayoutEffect 提交阶段的入口函数,我们看下是实在哪里执行的
js
// react-reconciler/src/ReactFiberWorkLoop.js
function commitRoot(root) {
const { finishedWork } = root;
const subtreeHasEffects = (finishedWork.subtreeFlags & MutationMask) !== NoFlags;
const rootHasEffect = (finishedWork.flags & MutationMask) !== NoFlags;
if (subtreeHasEffects || rootHasEffect) {
// commitWork 入口函数
commitMutationEffectsOnFiber(finishedWork, root);
// useLayoutEffect commitWork 阶段的入口函数
commitLayoutEffects(finishedWork, root);
if (rootDoseHavePassiveEffect) {
rootDoseHavePassiveEffect = false;
rootWithPendingPassiveEffects = root;
}
}
root.current = finishedWork;
}
所以 useLayoutEffect 是在 commitWork 执行完成之后立马就会执行,
作为对比 useEffect 提交阶段是在 flushPassiveEffects 中执行,而 flushPassiveEffects 是异步执行的
js
// react-reconciler/src/ReactFiberWorkLoop.js
function commitRoot(root) {
const { finishedWork } = root;
if (
(finishedWork.subtreeFlags & Passive) !== NoFlags ||
(finishedWork.flags & Passive) !== NoFlags
) {
if (!rootDoseHavePassiveEffect) {
rootDoseHavePassiveEffect = true;
// useEffect commitWork 阶段的入口函数,异步执行
scheduleCallback(flushPassiveEffects);
}
}
」
function flushPassiveEffects() {
if (rootWithPendingPassiveEffects !== null) {
const root = rootWithPendingPassiveEffects;
commitPassiveUnmountEffects(root.current);
commitPassiveMountEffects(root, root.current);
}
}
useLayoutEffect 函数是先于 useEffect 执行的,也就是说 useLayoutEffect 的 destroy 都执行完了之后会执行 useLayoutEffect 的 effect 函数
等到当前 useLayoutEffect 所有函数都执行完了之后才会一次执行 useEffect(这个所有指的是整个 Fiber 树的 useLayoutEffect 函数)
useLayoutEffect 的 effect 函数
useLayoutEffect 的 effect 函数是在 destroy 函数之后执行的,这里先看 effect 函数
commitLayoutEffects 函数是 useLayoutEffect 的入口函数,它的作用就是调用 commitLayoutEffectOnFiber 根据节点的 tag,递归处理,如果处理到了函数组件的话就调用 commitHookLayoutEffects 函数
执行 commitHookLayoutEffects 函数会传入 HookLayout,也就是说当前 Fiber 的 useLayoutEffect 的 effect 函数都会执行
js
// react-reconciler/src/ReactFiberCommitWork.js
// 递归处理 Fiber 节点,如果 Fiber 是函数组件的话,就调用 commitHookLayoutEffects 函数,处理 useLayoutEffect 的 effect 函数
function commitLayoutEffectOnFiber(finishedRoot, current, finishedWork) {
const flags = finishedWork.flags;
switch (finishedWork.tag) {
case FunctionComponent: {
recursivelyTraverseLayoutEffects(finishedRoot, finishedWork);
if (flags & Update) {
// useLayoutEffect 执行 mount 阶段的 effect 函数
commitHookLayoutEffects(finishedWork, HookLayout | HookHasEffect);
}
break;
}
default: {
recursivelyTraverseLayoutEffects(finishedRoot, finishedWork);
break;
}
}
}
function recursivelyTraverseLayoutEffects(root, parentFiber) {
if (parentFiber.subtreeFlags & LayoutMask) {
let child = parentFiber.child;
while (child !== null) {
const current = child.alternate;
commitLayoutEffectOnFiber(root, current, child);
child = child.sibling;
}
}
}
function commitHookLayoutEffects(finishedWork, hookFlags) {
commitHookEffectListMount(hookFlags, finishedWork);
}
useLayoutEffect 的 destroy 函数
useLayoutEffect 的 destroy 函数是在哪里执行的呢?
刚开始在看的源码的时候不理解,commitLayoutEffectOnFiber 函数中只有执行了 commitHookEffectListMount,这个函数是执行 effect 函数
那 destroy 函数是在哪里执行的呢,源码翻了很久才找到的
其实 useLayoutEffect 的 destroy 函数是在 commitWork 阶段执行的
commitMutationEffecsOnFiber 是 commitWork 的入口函数,react 在 FunctionComponent 分支中加了一行代码用来执行 useLayoutEffect 的 destroy 函数
js
// react-reconciler/src/ReactFiberCommitWork.js
function commitMutationEffectsOnFiber(finishedWork, root) {
const flags = finishedWork.flags;
const current = finishedWork.alternate;
switch (finishedWork.tag) {
case FunctionComponent: {
recursivelyTraverseMutationEffects(root, finishedWork);
commitReconciliationEffects(finishedWork);
// LayoutMask 中是有 Update 属性的,所以这里会执行 useLayoutEffect 的 destroy 函数
if (flags & Update) {
// useLayoutEffect 会先执行 unmount 阶段的 destroy 函数
commitHookEffectListUnmount(HookLayout | HookHasEffect, finishedWork);
}
break;
}
default: {
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
commitReconciliationEffects(finishedWork);
return;
}
}
}
总结
useLayoutEffect 和 useEffect 相同点:
- 他们会互相组成链表,保存在
updateQueue.lastEffect属性中 - 初始渲染时执行
effect函数,更新是先执行destroy函数,再执行effect函数
不同点:
useEffect是异步执行的,useLayoutEffect是同步执行的useEffect是在渲染后执行的,useLayoutEffect是在渲染时执行的useLayoutEffect所有函数(effect和destroy函数)都是在useEffect函数执行执行的useLayoutEffect的标识是HookLayout,useEffect的标识是HookPassive
源码
useEffect-beginWork阶段- useEffect-commitWork 阶段
useLayoutEffect-beginWork阶段useLayoutEffect-commitWork阶段