写在前面
本系列会实现一个简单的react,包含最基础的首次渲染,更新,hook,lane模型等等,本文是本系列的第一篇。这对于我也是一个很大的挑战,不过这也是一个学习和进步的过程,希望能坚持下去,一起加油!期待多多点赞!😘😘
本文致力于实现一个最简单的useEffect,代码均已上传至github,期待star!✨:
本文是系列文章,阅读的联系性非常重要!!
手写mini-react!超万字实现mount首次渲染流程🎉🎉
面试官问我 react scheduler 调度机制原理? 我却支支吾吾答不上来...😭😭
期待点赞!😁😁
食用前指南!本文涉及到react的源码知识,需要对react有基础的知识功底,建议没有接触过react的同学先去官网学习一下基础知识,再看本系列最佳!
一. 基本概念
在实现之前,先来看一下useEffect的基本使用,useEffect是react中非常重要,也是最常用的几个hook之一,它的出现可以使函数组件拥有类组件上的一些功能,比如生命周期函数,弥补了函数式组件没有生命周期的缺陷。并且提供了为某些依赖项增加副作用函数,当依赖项发生改变,触发副作用函数。
基本使用:
ts
useEffect(()=>{
return destory
}, deps)
参数:
callback:useEffect的第一个入参,最终返回destory,它会在下一次callback执行之前调用,其作用是清除上次的callback产生的副作用;deps:依赖项,可选参数,是一个数组,可以有多个依赖项,通过依赖改变,执行上一次的callback返回的destory和新的effect第一个参数callback。
挂载和卸载阶段触发
js
useEffect(() => {
console.log("挂载阶段");
return () => {
console.log("卸载阶段");
};
}, []);
如果依赖项传入一个空数组,那么useEffect分别会在挂载阶段和组件卸载阶段触发,这个效果可以替代类组件中的生命周期。
依赖项变化触发
js
useEffect(() => {
console.log("num改变执行");
}, [num]);
当num这个依赖项发生变化时,会执行副作用函数回调。
前面我们已经实现过useState这个hook,在动手实现useEffect之前先了解一下react的hook架构。
二. 数据共享层
hook架构在实现时,脱离了react部分的逻辑,在内部实现了一个数据共享层,类似于提供一个接口。任何满足了规范的函数都可以通过数据共享层接入处理hook的逻辑。这样就可以与宿主环境解耦,灵活性更高。
js
// 内部数据共享层
export const __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED = {
currentDispatcher
};
const currentDispatcher = {
current: null
};
currentDispatcher为我们本次实现的hook。
所以对应到我们的render流程以及hooks的应用,他们之间的调用关系是这样的:
hooks怎么知道当前是mount还是update?
我们在使用hooks时,react在内部通过 **currentDispatcher.current** 赋予不同的函数来处理不同阶段的调用,判断hooks 是否在函数组件内部调用。
三. hooks
hooks可以看做是函数组件和与其对应的fiber节点进行沟通和的操作的纽带。在react中处于不同阶段的fiber节点会被赋予不同的处理函数执行hooks:
- 初始化阶段 ----->
HookDispatcherOnMount - 更新阶段 ----->
HookDispatcherOnUpdate
js
const HookDispatcherOnMount = {
useState: mountState,
useEffect: mountEffect
};
const HookDispatcherOnUpdate = {
useState: updateState,
useEffect: updateEffect
};
但是实现之前,还有几个问题需要解决:
如何确定fiber对应的hooks上下文?
还记得我们在处理函数组件类型的fiber节点时,调用renderWithHooks函数进行处理,在我们在执行hooks相关的逻辑时,将当前fiber节点信息保存在一个全局变量中:
js
// 当前正在render的fiber
let currentlyRenderingFiber = null;
js
export function renderWithHooks(wip: FiberNode) {
// 赋值操作
currentlyRenderingFiber = wip;
// 重置
wip.memoizedState = null;
const current = wip.alternate;
if (current !== null) {
// update
// hooks更新阶段
} else {
// mount
// hooks初始化阶段
}
const Component = wip.type;
const props = wip.pendingProps;
const children = Component(props);
// 重置操作
// 处理完当前fiber节点后清空currentlyRenderingFiber
currentlyRenderingFiber = null;
return children;
}
将当前正在处理的fiber节点保存在全局变量currentlyRenderingFiber 中,我们在处理hooks 的初始化及更新逻辑中就可以获取到当前的fiber节点信息。
hooks是如何存在的?保存在什么地方?
注意hooks只存在于函数组件中,但是一个函数组件的fiber节点时如何保存hooks信息呢?
答案是:memoizedState。
fiber节点中保存着非常多的属性,有作为构造fiber链表,用于保存位置信息的属性,有作为保存更新队列的属性等等。
而对于函数组件类型的fiber节点,memoizedState属性保存hooks信息。hook在初始化时,会创建一个对象,保存此hook所产生的计算值,更新队列,hooks链表。
js
const hook = {
// hooks计算产生的值 (初始化/更新)
memoizedState: "";
// 对此hook的更新行为
updateQueue: "";
// hooks链表指针
next: null;
}
多个hook如何处理?
例如有以下代码:
js
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
const [age, setAge] = useState(10);
function handleClick() {
setCount(count + 1);
}
function handleAgeClick() {
setCount(age + 18);
}
return (
<button onClick={handleClick}>
add
</button>
<button onClick={handleAgeClick}>
age
</button>
);
}
在某个函数组件中存在多个hooks,此时每个hook的信息该如何保存呢?这就是上文中hook对象中next 属性的作用,它是一个链表指针。在hook对象中,next 属性指向下一个hook。
换句话说,如果在一个函数组件中存在多个hook,那么在该fiber节点的memoizedState属性中保存该节点的hooks链表。
函数组件对应 fiber 用 memoizedState 保存 hook 信息,每一个 hook 执行都会产生一个 hook 对象,hook 对象中,保存着当前 hook的信息,不同 hook保存的形式不同。每一个 hook 通过 next 链表建立起关系。
useEffect的hook链表结构
useEffect的hook链表对象与useState有一些差异,具体体现在useState的hook对象的memoizedState属性保存的是具体的值,而useEffect保存了一个对象:

对象中保存与useEffect相关的一些信息:create(依赖函数),destroy(卸载函数),deps(依赖项),next(下一个useEffect hook对象)。其中next属性也是一个链表指针,将该fiber下的所有useEffect hook对象连接在一起。
也就是说如果一个函数组件类型的fiber节点中存在许多不同类型的hook,那么这个fiber节点的memoizedState属性将会是这样的:


四. useEffect
在介绍react初始化流程时,首先在render阶段调用beginWork开始构建fiber节点,而对于不同的节点,react会进行不同的处理:
js
export const beginWork = (wip) => {
// 返回子fiberNode
switch (wip.tag) {
// 根节点
case HostRoot:
return updateHostRoot(wip);
// 原生dom节点
case HostComponent:
return updateHostComponent(wip);
// 文本节点
case HostText:
return null;
// 函数组件
case FunctionComponent:
return updateFunctionComponent(wip);
default:
if (__DEV__) {
console.warn('beginWork未实现的类型');
}
break;
}
return null;
};
针对函数节点,我们调用updateFunctionComponent函数进行处理,在初始化时,主要任务是执行函数组件,然后生成此函数组件的fiber节点,在上文中我们使用了两个函数来执行这两个任务:
js
function updateFunctionComponent(wip) {
// 执行函数组件,生成fiber节点
const nextChildren = renderWithHooks(wip);
// 根据fiber生成真实DOM节点
reconcilerChildren(wip, nextChildren);
return wip.child;
}
而针对于 renderWithHooks 函数来说,在我们加入hooks相关的逻辑后,显然它需要被承载更重要的能力,就是根据不同的处理阶段来为currentDispatcher.current赋值不同的hooks函数,之所以会在这个函数中处理,是因为可以直接避免在其他无关的环境里调用hooks函数。
currentDispatcher.current 在其他类型的fiber节点被处理时值都为null,这样就保证了hooks函数只在函数组件中被调用。
js
export const useEffect = (create, deps) => {
const dispatcher = resolveDispatcher();
return dispatcher.useEffect(create, deps);
};
当currentDispatcher.current为null时,说明当前并非函数组件。
js
const currentDispatcher = {
current: null
};
// 错误处理,当currentDispatcher.current为null时,说明当前并非函数组件
export const resolveDispatcher = () => {
const dispatcher = currentDispatcher.current;
if (dispatcher === null) {
throw new Error('hook只能在函数组件中执行');
}
return dispatcher;
};
在renderWithHooks中依旧根据alternate的存在判断当前为初始化/更新?
js
// 当前正在render的fiber
let currentlyRenderingFiber = null;
// 当前处理中的hooks
let workInProgressHook = null;
// 当前current树中的hooks
let currentHook = null;
export function renderWithHooks(wip) {
// 赋值操作
currentlyRenderingFiber = wip;
// 重置memoizedState
wip.memoizedState = null;
const current = wip.alternate;
if (current !== null) {
// update
currentDispatcher.current = HookDispatcherOnUpdate;
} else {
// mount
currentDispatcher.current = HookDispatcherOnMount;
}
// ....
// 重置操作
currentlyRenderingFiber = null;
workInProgressHook = null;
currentHook = null;
}
在renderWithHooks函数中定义了三个全局变量:currentlyRenderingFiber代表当前正在处理的fiber节点。workInProgressHook 代表当前处理中的hooks,也就是workInProgress树中的。currentHook 代表当前的current树中的hooks。
currentDispatcher.current 在初始化阶段和更新阶段分别被赋值给了HookDispatcherOnMount 和HookDispatcherOnUpdate,分别执行不同的逻辑。
js
const HookDispatcherOnMount = {
useEffect: mountEffect
};
const HookDispatcherOnUpdate = {
useEffect: updateEffect
};
flags
为了在fiber节点中标记副作用,增加PassiveEffect代表具有effect副作用的fiber节点。
Passive标记useEffect对象,HookHasEffect表示当前effect本次更新存在副作用。
js
// effect对象的标记
export const Passive = 0b0010;
export const HookHasEffect = 0b0001;
// fiber节点的标记
export const PassiveEffect = 0b0001000;
五. mount
在初始化阶段,useEffect需要开始初始化相关的数据结构:
- 创建hook对象并连接到原有的hook链表中
- 创建
effect对象保存到hook对象中的memoizedState属性 - 构建
effect链表,并保存到fiber.updateQueue.lastEffect属性中
js
function mountEffect(create, deps) {
// 创建hook对象
const hook = mountWorkInProgresHook();
const nextDeps = deps === undefined ? null : deps;
// 为fiber标记flag
currentlyRenderingFiber.flags |= PassiveEffect;
// 生成effect对象,并保存effect链表
hook.memoizedState = pushEffect(
// 标记effect对象的flag
Passive | HookHasEffect,
create,
undefined,
nextDeps
);
}
在mount阶段创建主要考虑当前执行的useEffect函数是当前函数组件的第几个,如果是第一个需要初始化给fiber节点的memoizedState属性并赋值给全局变量workInProgressHook。如果是后续的useEffect,直接使用全局变量workInProgressHook的next绑定。
js
function mountWorkInProgresHook() {
// 定义hook对象
const hook = {
memoizedState: null,
updateQueue: null,
next: null
};
if (workInProgressHook === null) {
// mount时 第一个hook
if (currentlyRenderingFiber === null) {
throw new Error('请在函数组件内调用hook');
} else {
workInProgressHook = hook;
currentlyRenderingFiber.memoizedState = workInProgressHook;
}
} else {
// mount时 后续的hook
workInProgressHook.next = hook;
workInProgressHook = hook;
}
return workInProgressHook;
}
pushEffect函数用来创建effect对象,并且将同一个函数组件内的多个effect对象连接成链表的形式,保存在函数组件的fiber节点的updateQueue.lastEffect中,在lastEffect中保存最后一个effect对象,但是由于effect链表为环形链表,所以也就相当于将整个effect链表保存在了lastEffect属性。
js
function pushEffect(
hookFlags,
create,
destroy,
deps
) {
const effect = {
tag: hookFlags,
create,
destroy,
deps,
next: null
};
const fiber = currentlyRenderingFiber;
const updateQueue = fiber.updateQueue;
// 如果无更新队列updateQueue?
// 1. 创建fiber节点的更新队列
// 2. 创建lastEffect属性保存effect链表
if (updateQueue === null) {
const updateQueue = createFCUpdateQueue();
fiber.updateQueue = updateQueue;
effect.next = effect;
updateQueue.lastEffect = effect;
} else {
// 插入effect
const lastEffect = updateQueue.lastEffect;
// 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;
}
如果当前fiber节点(函数组件类型的fiber节点)没有updateQueue属性,那么先创建updateQueue属性,与普通的fiber节点的更新队列略有不同,函数组件类型的fiber节点多个一个lastEffect保存effect链表:
js
function createFCUpdateQueue<State>() {
const updateQueue = createUpdateQueue();
// 创建lastEffect属性
updateQueue.lastEffect = null;
return updateQueue;
}
// 创建updateQueue
export const createUpdateQueue = () => {
return {
shared: {
pending: null
},
dispatch: null
};
};
如果是链表的第一项,为了创建环形链表,需要将next指针指向自身,也就是整个链表的头尾节点都是同一个:
js
effect.next = effect;
六. update
update阶段主要是对比两棵fiber树中对应的hook对象和effect对象是否发生了变化。
hook对象更新前后有无变化?检查是否能够复用effect对象更新前后有无变化?检查是否能够复用
effect对象是否可以复用的判断依据是根据比较依赖项(浅比较),确定新的effect对象是否加入HookHasEffect这个flag标记,如果依赖项没有变化,则不加入副作用标记。
destroy属性保存上次副作用函数的执行结果,直接在effect对象中获取。
js
function updateEffect(create, deps) {
// 复用hook对象,构建新的hook链表
const hook = updateWorkInProgresHook();
const nextDeps = deps === undefined ? null : deps;
let destroy;
// currentHook 全局变量,原fiber树对应的hook对象
if (currentHook !== null) {
// 获取原effect对象,destroy卸载函数是上次执行create时保存的执行结果
const prevEffect = currentHook.memoizedState;
// 获取卸载函数
destroy = prevEffect.destroy;
if (nextDeps !== null) {
// 浅比较依赖
const prevDeps = prevEffect.deps;
// 依赖项发生变化?
if (areHookInputsEqual(nextDeps, prevDeps)) {
hook.memoizedState = pushEffect(Passive, create, destroy, nextDeps);
return;
}
}
// 浅比较 不相等
currentlyRenderingFiber.flags |= PassiveEffect;
hook.memoizedState = pushEffect(
Passive | HookHasEffect,
create,
destroy,
nextDeps
);
}
}
对比依赖项:
js
function areHookInputsEqual(nextDeps, prevDeps) {
if (prevDeps === null || nextDeps === null) {
return false;
}
for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
// 使用Object.is浅比较
if (Object.is(prevDeps[i], nextDeps[i])) {
continue;
}
return false;
}
return true;
}
updateWorkInProgresHook函数复用hook对象。值得注意的是,如果在执行更新时获取某一个useEffect旧的hook对象为空,则说明本次更新比原来多了一个hook,
这也是hook不能用在条件判断中的原因,因为条件判断可能会存在动态逻辑,在更新时会导致hook对象无法对应。
js
function updateWorkInProgresHook() {
let nextCurrentHook;
if (currentHook === null) {
// 这是这个函数组件 update时的第一个hook
const current = currentlyRenderingFiber?.alternate;
if (current !== null) {
nextCurrentHook = current?.memoizedState;
} else {
// mount
nextCurrentHook = null;
}
} else {
// 这个函数组件 update时 后续的hook
nextCurrentHook = currentHook.next;
}
// 本次有多余hook
if (nextCurrentHook === null) {
throw new Error(
`组件${currentlyRenderingFiber?.type}本次执行时的Hook比上次执行时多`
);
}
currentHook = nextCurrentHook;
// 复用hook对象
const newHook = {
memoizedState: currentHook.memoizedState,
updateQueue: currentHook.updateQueue,
next: null
};
if (workInProgressHook === null) {
// mount时 第一个hook
if (currentlyRenderingFiber === null) {
throw new Error('请在函数组件内调用hook');
} else {
workInProgressHook = newHook;
currentlyRenderingFiber.memoizedState = workInProgressHook;
}
} else {
// 后续的hook 使用next连接
workInProgressHook.next = newHook;
workInProgressHook = newHook;
}
return workInProgressHook;
}
七. useEffect工作流程

commit阶段开始调度副作用,收集回调,在此之前我们要在根节点的属性中新增两个集合,用于收集unmout时需要执行的回调和update时需要执行的回调。
diff
export class FiberRootNode {
// ...
pendingPassiveEffects: PendingPassiveEffects;
constructor(container, hostRootFiber) {
// ...
++ this.pendingPassiveEffects = {
++ unmount: [],
++ update: []
++ };
}
}
1. 调度副作用
在进入commit阶段开始后,也就是进入commitRoot函数,判断根节点中是否存在PassiveMask这个标记,代表需要调度执行保存在根节点的副作用函数,然后以普通优先级调度一个任务:
由于调度副作用是异步执行,所以虽然任务会先开始调度,但是收集回调的逻辑会先执行。
js
let rootDoesHasPassiveEffects = false;
function commitRoot(root) {
// ...
if (
(finishedWork.flags & PassiveMask) !== NoFlags ||
(finishedWork.subtreeFlags & PassiveMask) !== NoFlags
) {
if (!rootDoesHasPassiveEffects) {
rootDoesHasPassiveEffects = true;
// 调度副作用
scheduleCallback(NormalPriority, () => {
// 执行副作用
flushPassiveEffects(root.pendingPassiveEffects);
return;
});
}
}
// ...
commitMutationEffects(finishedWork, root);
}
本次更新的任何create回调都必须在所有上一次更新的destroy回调执行完后再执行。
整体执行流程包括:
- 遍历
effect链表 - 首先触发所有
unmount effect,且对于某个fiber,如果触发了unmount destroy,本次更新不会再触发update create - 触发所有上次更新的
destroy - 触发所有这次更新的
create
mount、update时的区别:
mount时:标记PassiveEffectupdate时:deps变化时标记PassiveEffect
js
function flushPassiveEffects(pendingPassiveEffects) {
// 执行unmount集合
pendingPassiveEffects.unmount.forEach((effect) => {
commitHookEffectListUnmount(Passive, effect);
});
pendingPassiveEffects.unmount = [];
// 执行update集合
pendingPassiveEffects.update.forEach((effect) => {
commitHookEffectListDestroy(Passive | HookHasEffect, effect);
});
// 执行update集合
pendingPassiveEffects.update.forEach((effect) => {
commitHookEffectListCreate(Passive | HookHasEffect, effect);
});
pendingPassiveEffects.update = [];
flushSyncCallbacks();
}
在useEffect执行时,关于副作用回调和销毁函数会涉及这么几种情况:函数组件被销毁,此时应该执行所有的销毁函数,由于此时整个函数组件已经被销毁,后续也不应该再执行副作用函数,所以移除unmount列表中所有effect对象的HookHasEffect标记。
js
// 任务执行
function commitHookEffectList(flags, lastEffect, callback) {
let effect = lastEffect.next
// 循环执行effect链表
do {
if ((effect.tag & flags) === flags) {
callback(effect);
}
effect = effect.next
} while (effect !== lastEffect.next);
}
export function commitHookEffectListUnmount(flags, lastEffect) {
commitHookEffectList(flags, lastEffect, (effect) => {
const destroy = effect.destroy;
if (typeof destroy === 'function') {
destroy();
}
// 移除HookHasEffect标记,后面不会再执行副作用函数
effect.tag &= ~HookHasEffect;
});
}
更新任务先触发所有上次更新的destroy函数,执行完后,触发所有这次更新的create函数。本次更新产生的返回值将作为新的effect对象的destroy。
js
export function commitHookEffectListDestroy(flags, lastEffect) {
commitHookEffectList(flags, lastEffect, (effect) => {
const destroy = effect.destroy;
if (typeof destroy === 'function') {
destroy();
}
});
}
export function commitHookEffectListCreate(flags, lastEffect) {
commitHookEffectList(flags, lastEffect, (effect) => {
const create = effect.create;
if (typeof create === 'function') {
// 将执行结果保存为destroy
effect.destroy = create();
}
});
}
2. 收集回调
关于收集回调的时机,在进入commit流程后,首先还要从上到下,从下到上的回溯一遍fiber树,目的是根据fiber上的不同标记开始对fiber节点中保存的dom进行操作,比如:新增,更新,删除等。(详细参考前面初始化急更新的文章)
之所以会选择在commit阶段的回溯过程中收集,我们先来看一个例子:
js
export default 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";
}
在这个例子中父子组件同时定义了useEffect,那么它的执行顺序是什么样子的呢?

根据mount阶段的执行结果可知,在初始化时,先执行了子组件Child中useEffect函数的初始化,同样副作用函数也会先执行,所以在收集副作用函数时,子组件的effect副作用函数必要要先被收集。在不同层级的函数组件中遵循先子后父的执行顺序,如果是同一层级则按顺序执行。
按照useEffect的执行顺序,我们在向上回溯的过程中收集effect。
js
const commitMutaitonEffectsOnFiber = (
finishedWork,
root
) => {
const flags = finishedWork.flags;
if ((flags & Placement) !== NoFlags) {
// ...
}
if ((flags & Update) !== NoFlags) {
// ...
}
if ((flags & ChildDeletion) !== NoFlags) {
// ...
}
if ((flags & PassiveEffect) !== NoFlags) {
// 收集更新回调
commitPassiveEffect(finishedWork, root, 'update');
// 收集回调后,移除副作用标记
finishedWork.flags &= ~PassiveEffect;
}
};
收集的过程比较简单,获取fiber节点中的updateQueue.lastEffect属性,根据type保存在根节点的pendingPassiveEffects中。
js
function commitPassiveEffect(fiber, root, type) {
// update unmount
if (
fiber.tag !== FunctionComponent ||
(type === 'update' && (fiber.flags & PassiveEffect) === NoFlags)
) {
return;
}
const updateQueue = fiber.updateQueue
if (updateQueue !== null) {
if (updateQueue.lastEffect === null && __DEV__) {
console.error('当FC存在PassiveEffect flag时,不应该不存在effect');
}
root.pendingPassiveEffects[type].push(updateQueue.lastEffect);
}
}
unmount列表收集过程在节点删除的流程中进行。
js
function commitDeletion(childToDelete, root) {
const rootChildrenToDelete = [];
// 递归子树
commitNestedComponent(childToDelete, (unmountFiber) => {
switch (unmountFiber.tag) {
case HostComponent:
// ...
return;
case HostText:
// ...
return;
case FunctionComponent:
// 收集卸载函数
commitPassiveEffect(unmountFiber, root, 'unmount');
return;
default:
if (__DEV__) {
console.warn('未处理的unmount类型', unmountFiber);
}
}
});
}
当一个节点被删除或卸载时,不仅仅要删除此节点本身的各种副作用,还需要对所有子节点进行卸载。commitNestedComponent会针对待删除节点的每个子fiber节点执行回调函数。收集待删除节点的所有子节点的卸载函数。
写在最后 ⛳
未来可能会更新实现mini-react和antd源码解析系列,希望能一直坚持下去,期待多多点赞🤗🤗,一起进步!🥳🥳