本系列是讲述从0开始实现一个react18的基本版本。通过实现一个基本版本,让大家深入了解React
内部机制。
由于React
源码通过Mono-repo 管理仓库,我们也是用pnpm
提供的workspaces
来管理我们的代码仓库,打包我们使用rollup
进行打包。
系列文章:
- React实现系列一 - jsx
- 剖析React系列二-reconciler
- 剖析React系列三-打标记
- 剖析React系列四-commit
- 剖析React系列五-update流程
- 剖析React系列六-dispatch update流程
- 剖析React系列七-事件系统
- 剖析React系列八-同级节点diff
- 剖析React系列九-Fragment的部分实现逻辑
- 剖析React系列十- 调度<合并更新、优先级>
在上一节中,我们初步了解了微任务下面的合并更新以及优先级策略。本章我们来讲解useEffect
的实现逻辑。
useEffect(() => {
console.log('mount')
return () => {
console.log('unMount')
}
}, [])
在useEffect
中,结构是要分3个部分:其中回调函数的执行是在异步调用
。
create
函数destroy
函数- 依赖项的保存
deps
useEffect
数据结构和保存位置
在剖析React系列六-dispatch update流程中,我们了解到每一个fiber
对应的fiberNode.memoizedState
指向hook
的链表。 每一个hook
中的hook.memoizedState
对应当前hook
的结构。
例如对应useState
中,例如有两个useState
调用, 并且一定的条件下点击触发三次更新:
const [num, setNum] = useState(100)
const [count, setCount] = useState(1)
/ 点击触发三次
setNum(num + 1);
setNum(num + 2);
setNum((num) => num + 2);
effect
数据结构
当我们设计useEffect
的数据结构的时候需要注意几个部分:
- 不同effect可以共用一个机制,所以需要
tag
区分不同的effect
useEffect / useLayoutEffect / useInsertionEffect
- 需要保存依赖,用于对比依赖是否变化
- 需要保存
create
回调 - 需要保存
destroy
回调 - 需要能够区分是否需要触发
create
回调mount
时- 依赖变化时
const effect = {
tag,
create,
destroy,
deps,
next
}
effect
对应的flag
在之前的章节中,我们添加过Placement
以及 Update
等flags, 用于标记对应的操作。本节新增几个对应flag
。
对于fiberNode 节点
PassiveEffect
: 表示当前fiber有effect
的副作用
PassiveMask
: 表示存在需要触发effect
。 PassiveEffect | ChildDeletion
对于effect hook
Passive
:useEffect
对应的effect
HookHasEffect
:当前effect
存在副作用
例如:上图右边所示,当
useEffect
的tag
标记为Passive | HookHasEffect
的时候,表示当前的hook存在副作用需要执行,所以hook对应的fiberNode
就需要标记为HookHasEffect
。
effect
自成环状
在之前的章节中,我们已经了解到hook
之前会通过next
指针来连接,这样在更新的过程中,可以很快的得到一个hook
列表。
在effect
的hook中,为了方便单独的effect
的使用,所以effect
中hook.memoizedState
中有一个next
属性
在上一节的合并更新中,我们晓得updateQueue.shared.pending
中存放的useSate
的action
集合自成环装,方便我们遍历去执行相应的操作。
其中主要逻辑都存在pushEffect
中, 它主要是根据tag
新建effect
,并将fiber.updateQueue
指向环状列表。fiber.updateQueue.lastEffect
指向最后一个effect
方便之后收集回到的时候只遍历effect
。
hook.memoizedState = pushEffect(
Passive | HookHasEffect,
create,
undefined,
nextDeps
);
// ......
function pushEffect(
hookFlags: Flags,
create: EffectCallback | void,
destroy: EffectCallback | void,
deps: EffectDeps
) {
const effect: Effect = {
tag: hookFlags,
create,
destroy,
deps,
next: null,
};
const fiber = currentlyRenderingFiber as FiberNode;
const updateQueue = fiber.updateQueue as FCUpdateQueue<any>;
if (updateQueue === null) {
const updateQueue = createFCUpdateQueue();
fiber.updateQueue = updateQueue;
effect.next = effect;
updateQueue.lastEffect = effect;
} else {
// 插入effect
const lastEffect = updateQueue.lastEffect;
if (lastEffect === null) {
effect.next = effect;
updateQueue.lastEffect = effect;
} else {
const firstEffect = lastEffect.next;
lastEffect.next = effect;
effect.next = firstEffect;
updateQueue.lastEffect = effect;
}
}
return effect;
}
Effect的工作流程
上面我们主要介绍了effect
的数据格式,接下来我们讲解如何从现有的工作流中接入effect
。
分三个阶段:
render
阶段 判断是否存在effect
副作用commit
阶段,异步调度副作用、收集回调- 主流程和微任务合并更新执行完后,开始执行回调(类似
setTimeout
)
调度副作用
在commit阶段
进入的时候,判断当前节点是否存在副作用,然后通过scheduleCallback
包进行调度流程。
function commitRoot(root: FiberRootNode) {
// xxx
// 当前Fiber树中存在函数组件需要执行useEffect的回调
if (
(finishedWork.flags & PassiveMask) !== NoFlags ||
(finishedWork.subtreeFlags & PassiveMask) !== NoFlags
) {
// 防止多次调用
if (!rootDoesHasPassiveEffects) {
rootDoesHasPassiveEffects = true;
// 调度副作用
scheduleCallback(NormalPriority, () => {
// 执行副作用
flushPassiveEffects(root.pendingPassiveEffects);
return;
});
}
}
// ....commit阶段
commitMutationEffects(finishedWork, root);
}
这里的scheduleCallback
来自于react
官方提供的调度器,这里我们可以把它简单的理解为内部回调函数相等于setTimeout
中执行。
useEffect的逻辑
分为mount
、update
二种情况,他们的区别:
mount
时:一定标记PassiveEffect
update
时:deps
变化时标记PassiveEffect
mount阶段
在mount
阶段,我们晓得useEffect
的create
函数一定会执行,所以我们需要在mount
的时候收集create
执行的回调。
function mountEffect(create: EffectCallback | void, deps: EffectDeps | void) {
// 新建hook
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
(currentlyRenderingFiber as FiberNode).flags |= PassiveEffect;
hook.memoizedState = pushEffect(
Passive | HookHasEffect,
create,
undefined,
nextDeps
);
}
主要逻辑是在pushEffect
中,生成effect
特有的数据格式,此时由于effect
是存在副作用的,所以传入的tag
为Passive | HookHasEffect
。表示是useEffect
并且还需要执行回调函数。初始化阶段不存在destroy
的执行,所以传入undifined
。
同时将对应的fiberNode
标记为PassiveEffect
,表示有副作用要执行。
pushEffect
的代码查看上面effect
自成环状。
update
阶段
在update
阶段。我们实际执行的useEffect
对应updateEffect
。这个阶段
function updateEffect(create: EffectCallback | void, deps: EffectDeps | void) {
// 对应去mount的时候的每一个effect
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
let destroy: EffectCallback | void;
if (currentHook !== null) {
const prevEffect = currentHook.memoizedState as Effect;
destroy = prevEffect.destroy;
if (nextDeps !== null) {
// 浅比较依赖
const prevDeps = prevEffect.deps;
if (areHookInputsEqual(nextDeps, prevDeps)) {
hook.memoizedState = pushEffect(Passive, create, destroy, nextDeps);
return;
}
}
// 浅比较后不相等
(currentlyRenderingFiber as FiberNode).flags |= PassiveEffect;
hook.memoizedState = pushEffect(
Passive | HookHasEffect,
create,
destroy,
nextDeps
);
}
}
在update
阶段主要是对比前后依赖项,如果发生了变化的话,传入create
以及destroy
的同时,需要传入标记Passive | HookHasEffect
,如果没有变动的话,就是传入Passive
。
这里获取更新前的hook数据是通过currentHook.memoizedState
。
收集回调
如果存在副作用,在之后执行flushPassiveEffects
之前,我们肯定要先去收集那些effect
存在副作用,以及存在那些副作用(create
/ destroy
)。
了解effect
的使用的话,我们就应该晓得主要是分为2种回调收集:
unmout
时执行的destroy
回调update
/mount
时执行的create
回调
我们将需要收集的回调存放在fiberRootNode
的pendingPassiveEffects
属性中。
export interface PendingPassiveEffects {
unmount: Effect[];
update: Effect[];
}
在useEffect
的初始化和更新执行的时候,如果存在副作用的话,就会标记对应的fiber.flag
为PassiveEffect
。 所以我们可以在comit
阶段通过当前的fiber是否存在PassiveEffect
的标记判断是否需要收集回调。
收集update
回调
在commitMutationEffects
的过程中,当进行向上遍历的时候,我们会执行commitMutationEffectsOnFibers
。
由于我们是从下到上的遍历, 所以在之后的回调执行的时候,也会先执行子元素,之后才是父元素。正好对应React
源码。
const commitMutationEffectsOnFibers = (
finishedWork: FiberNode,
root: FiberRootNode
) => {
const flags = finishedWork.flags;
// flags childDeletion
if ((flags & ChildDeletion) !== NoFlags) {
// 删除
}
if ((flags & PassiveEffect) !== NoFlags) {
// 收集回调
commitPassiveEffect(finishedWork, root, "update");
finishedWork.flags &= ~PassiveEffect;
}
}
判断当前的fiber.flag
是否存在PassiveEffect
,然后执行收集的函数commitPassiveEffect
。
commitPassiveEffect
主要的功能就是根据传入进来的type
,将当前的函数fiber
对应的hook的环状列表放入root.pendingPassiveEffects
中。
function commitPassiveEffect(
fiber: FiberNode,
root: FiberRootNode,
type: keyof PendingPassiveEffects
) {
//update unmount
if (
fiber.tag !== FunctionComponent ||
(type === "update" && (fiber.flags & PassiveEffect) === NoFlags)
) {
return;
}
const updateQueue = fiber.updateQueue as FCUpdateQueue<any>;
if (updateQueue !== null) {
if (updateQueue.lastEffect === null && __DEV__) {
console.error("当FC存在PassiveEffect flags时,不应该不存在effect");
}
root.pendingPassiveEffects[type].push(updateQueue.lastEffect as Effect);
}
}
因此,在标记了PassiveEffect
的fiber的中, 我们收集了的root.pendingPassiveEffects[update
],其中包含create
以及未销毁的fiber的destroy
的回调函数,
收集destroy
回调
在之前的章节中,我们删除节点阶段的执行是在commitDeletion
中,对应的函数组件处理的时候,我们通过commitPassiveEffect
将塞入对应的unmount
的类型回调函数。
/**
* 删除对应的子fiberNode
* @param {FiberNode} childToDelete
*/
function commitDeletion(childToDelete: FiberNode, root: FiberRootNode) {
const rootChildrenToDelete: FiberNode[] = [];
// 递归子树
commitNestedComponent(childToDelete, (unmountFiber) => {
switch (unmountFiber.tag) {
case HostComponent:
// ......
case HostText:
// ......
case FunctionComponent:
commitPassiveEffect(unmountFiber, root, "unmount");
return;
default:
// ......
}
});
// .......
}
在commitDeletion
中,我们主要是收集root.pendingPassiveEffects[unmount
], 都是销毁的节点的destroy回调函数执行。
至此,我们就收集到了update
以及destroy
的回调函数。
接下来主流程执行完成后,开始执行对应的回调。
执行回调
在主流程和微任务合并更新执行完后,开始执行刚刚收集的回调函数。 但是回调函数的执行是有顺序的。
本次更新的任何create
回调都必须在所有上一次更新的destroy
回调执行完后再执行。
- 遍历
effect
- 首先触发所有
unmount effect
,且对于某个fiber
,如果触发了unmount destroy
,本次更新不会再触发update create
- 触发所有上次更新的
destroy
- 触发所有这次更新的
create
主要逻辑都存在flushPassiveEffects
中。由于flushPassiveEffects
的执行是在新开的一个宏任务中,所以我们常说useEffect
是一个异步操作。
function flushPassiveEffects(pendingPassiveEffects: PendingPassiveEffects) {
// unmount effect
pendingPassiveEffects.unmount.forEach((effect) => {
commitHookEffectListUnmount(Passive, effect);
});
pendingPassiveEffects.unmount = [];
pendingPassiveEffects.update.forEach((effect) => {
commitHookEffectListDestroy(Passive | HookHasEffect, effect);
});
pendingPassiveEffects.update.forEach((effect) => {
commitHookEffectListCreate(Passive | HookHasEffect, effect);
});
pendingPassiveEffects.update = [];
flushSyncCallbacks();
}
对于commitHookEffectListUnmount
和commitHookEffectListDestroy
的区别是,commitHookEffectListUnmount
需要去掉HookHasEffect
,卸载后其他函数不要执行。
commitHookEffectListCreate
中将新创建的destroy
复制到effect.destroy
中,方便下次调用。
export function commitHookEffectListUnmount(flags: Flags, lastEffect: Effect) {
commitHookEffectList(flags, lastEffect, (effect) => {
const destroy = effect.destroy;
if (typeof destroy === "function") {
destroy();
}
effect.tag &= ~HookHasEffect;
});
}
export function commitHookEffectListDestroy(flags: Flags, lastEffect: Effect) {
commitHookEffectList(flags, lastEffect, (effect) => {
const destroy = effect.destroy;
if (typeof destroy === "function") {
destroy();
}
});
}
export function commitHookEffectListCreate(flags: Flags, lastEffect: Effect) {
commitHookEffectList(flags, lastEffect, (effect) => {
const create = effect.create;
if (typeof create === "function") {
effect.destroy = create();
}
});
}
例子
在下面例子中, 我们创建了App
组件,然后有一个<Child />
。
function App() {
const [num, updateNum] = useState(0);
useEffect(() => {
console.log("App mount");
}, []);
useEffect(() => {
console.log("num change create", num);
return () => {
console.log("num change destroy", num);
};
}, [num]);
return (
<div onClick={() => updateNum(num + 1)}>
{num === 0 ? <Child /> : "noop"}
</div>
);
}
function Child() {
useEffect(() => {
console.log("Child mount");
return () => {
console.log("Child unmount");
};
}, []);
return "i am child";
}
在初始化的时候会依次执行:
- Child mount
- App mount
- "num change create", 0
点击后,Child
组件开始卸载,由于首先执行unmount
回调,顺序依次是:
- Child unmount
- num change destroy 0
- num change create 1
简单的结构图如下:从Child-fiberNode
开始向上遍历。