Skip to main content

3 posts tagged with "fiber"

View All Tags

· 14 min read

在 React 中,数据获取、设置订阅以及手动更改 React 组件中的 DOM 都属于副作用。在 React 16.8 新增了 Hook,这些副作用都可以通过 useEffect 或者 useLayoutEffect 来调用。前阵子抽空学了下 useState 的源码和做了总结,今天也稍微总结了下 use(Layout)Effect。

写在前面​

不熟悉 Fiber 架构的话可以参考 https://blog.zhouweibin.top/detail/60aa954b6da89c0011cca404

对 useState 原理感兴趣的可以参考我之前写的博客 https://blog.zhouweibin.top/detail/60a16c423ba50e0011ffbc3b

涉及到上面知识的本文可能不会赘述,下面主要重点谈谈 use(Layout)Effect 这两个 Hooks 在 React 挂载和更新过程中做的事情

effectList​

这是一个副作用链表,会连接具有副作用 effect 的 fiber 节点,副作用的数据格式如下

{
create: Function, // 传入use(Layout)Effect函数的第一个参数,即回调函数
destroy: Function, // 回调函数return的函数,在组件销毁的时候执行
deps: any[], // 依赖项
next: Effect, // 指向下一个effect
tag: string, // effect的类型,区分是useEffect还是useLayoutEffect
}

副作用在函数组件中体现为 use(Layout)Effect 的回调和销毁函数(即回调的返回值)(统称为副作用)

收集​

从单个 fiber 节点看,fiber.memorizedState 挂载着 Hooks 链表,Hooks 链表会按顺序连接该节点上的 Hook,其中 use(Layout)Effect 上的 memorizedState 会存放副作用,待执行的副作用会串联成一个循环链表存储在updateQueue上,如下图所示:

从整个 Fiber Tree 来看,render 阶段会自下而上创建 effectList,主要是通过completeUnitOfWork这个方法创建,只有依赖项有变化才会将当前的 fiber 节点追加到 effectList。如下图所示:

最终的 effectList 如下所示:

首次挂载组件(mount),执行 useEffect 时,会调用mountEffectImpl,简化后的源码如下:

function mountEffectImpl(fiberFlags, hookFlags, create, deps) {
const hook = mountWorkInProgressHook(); // 创建hook对象
const nextDeps = deps === undefined ? null : deps; // 依赖

// NOTE 给 fiber 节点打上副作用的 effectTag
currentlyRenderingFiber.flags = fiberFlags;

// 创建 effect 对象,挂载到 hook 的 memoizedState 上
hook.memoizedState = pushEffect(
HookHasEffect | hookFlags,
create,
undefined,
nextDeps
);
}

更新时(update),会调用 updateEffectImpl

function updateEffectImpl(fiberFlags, hookFlags, create, deps): void {
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
let destroy = undefined;

if (currentHook !== null) {
const prevEffect = currentHook.memoizedState;
// 获取上一次 effect 的 destroy 函数
destroy = prevEffect.destroy;
if (nextDeps !== null) {
const prevDeps = prevEffect.deps;
// NOTE 如果前后依赖相同
if (areHookInputsEqual(nextDeps, prevDeps)) {
pushEffect(hookFlags, create, destroy, nextDeps);
return;
}
}
}

// NOTE flags的作用
currentlyRenderingFiber.flags |= fiberFlags;
// 如果前后依赖有变,在 effect 的 tag 中加入 HookHasEffect
// 并将新的 effect 挂载到 hook.memoizedState 上
hook.memoizedState = pushEffect(
HookHasEffect | hookFlags,
create,
destroy,
nextDeps
);
}

组件 mount 和 update 时收集 effect 的区别在于,前者无须调用前一次的销毁函数,后者在创建 effect 前会判断依赖项,主要依赖项变化才会创建 effect 并挂载到 hook 上

不管组件处于什么状态,都会调用到pushEffect这个函数它的作用其实就是创建 effect 对象,将 effect 对象追加到 fiber 的 updateQueue 上

function pushEffect(tag, create, destroy, deps) {
const effect: Effect = {
tag,
create,
destroy,
deps,
next,
};

// NOTE 从 workInProgress 上取到 updateQueue(和 effectList 的区别)
let componentUpdateQueue = currentlyRenderingFiber.updateQueue;
if (componentUpdateQueue === null) {
// 如果 updateQueue 为空,把 effect 放到链表中,和它自己形成闭环
componentUpdateQueue = createFunctionComponentUpdateQueue();
currentlyRenderingFiber.updateQueue = componentUpdateQueue;
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
// updateQueue 不为空,将 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;
}
}
return effect;
}

在执行完 fiber 节点时,如果有更新 effect,会将该 fiber 节点追加到父节点的 effectList

function completeUnitOfWork(unitOfWork: Fiber): void {
let completedWork = unitOfWork;
do {
const current = completedWork.alternate;
const returnFiber = completedWork.return;

let next = completeWork(current, completedWork, subtreeRenderLanes);

// effect list构建
if (returnFiber !== null && (returnFiber.flags & Incomplete) === NoFlags) {
// 层层拷贝
if (returnFiber.firstEffect === null) {
returnFiber.firstEffect = completedWork.firstEffect;
}
if (completedWork.lastEffect !== null) {
// 说明当前节点是兄弟节点,子节点有effect,已经给returnFiber.lastEffect赋值过了
if (returnFiber.lastEffect !== null) {
// 连接兄弟节点的effect
returnFiber.lastEffect.nextEffect = completedWork.firstEffect;
}
returnFiber.lastEffect = completedWork.lastEffect;
}

const flags = completedWork.flags;

// NOTE: 该 fiber 节点有 effect
if (flags > PerformedWork) {
// 当前节点有effect连接上effect list
if (returnFiber.lastEffect !== null) {
returnFiber.lastEffect.nextEffect = completedWork;
} else {
// returnFiber没有firstEffect的情况是第一次遇见有effect的节点
returnFiber.firstEffect = completedWork;
}
returnFiber.lastEffect = completedWork;
}
}

// 兄弟元素遍历再到返返回父级
const siblingFiber = completedWork.sibling;
if (siblingFiber !== null) {
workInProgress = siblingFiber;
return;
}
completedWork = returnFiber;
workInProgress = completedWork;
} while (completedWork !== null);
}

执行​

render 阶段 useEffect 和 useLayoutEffect 没什么区别,在 commit 阶段会有比较明显的区别。先看下 useEffect,主要有三个地方会执行

useEffect​

  1. 刚进入 commit 阶段
function commitRootImpl(root, renderPriorityLevel) {
// 进入commit阶段,先执行一次之前未执行的 useEffect
do {
flushPassiveEffects();
} while (rootWithPendingPassiveEffects !== null);
// ...
do {
try {
// beforeMutation阶段的处理函数:commitBeforeMutationEffects内部,
// 异步调度 useEffect
commitBeforeMutationEffects();
} catch (error) {
...
}
} while (nextEffect !== null);
// ...
const rootDidHavePassiveEffects = rootDoesHavePassiveEffects;

if (rootDoesHavePassiveEffects) {
// NOTE 记录有副作用的 effect
rootWithPendingPassiveEffects = root;
}
}
  1. beforeMutation,该阶段会异步调度 useEffect
function commitBeforeMutationEffects() {
while (nextEffect !== null) {
// ...
if ((flags & Passive) !== NoFlags) {
// 如果fiber节点上的flags存在Passive调度useEffect
if (!rootDoesHavePassiveEffects) {
// 锁住调度状态,防止多次调度
rootDoesHavePassiveEffects = true;
// NOTE NormalSchedulerPriority 一般优先级
scheduleCallback(NormalSchedulerPriority, () => {
flushPassiveEffects();
return null;
});
}
}
nextEffect = nextEffect.nextEffect;
}
}
  1. layout 阶段填充 effect 执行数组,真正执行 useEffect 的时候,实际上是先执行上一次 effect 的销毁函数,再执行本次 effect 的创建函数
function commitLifeCycles(
finishedRoot: FiberRoot,
current: Fiber | null,
finishedWork: Fiber,
committedLanes: Lanes,
): void {
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case SimpleMemoComponent:
case Block: {
// ...
// layout阶段填充effect执行数组
schedulePassiveEffects(finishedWork);
return;
}
}

在调用 schedulePassiveEffects 填充 effect 执行数组时,有一个重要的地方就是只在包含 HasEffect 的 effectTag 的时候,才将 effect 放到数组内,这一点保证了依赖项有变化再去处理 effect。也就是:如果前后依赖未变,则 effect 的 tag 就赋值为传入的 hookFlags,否则,在 tag 中加入 HookHasEffect 标志位。正是因为这样,在处理 effect 链表时才可以只处理依赖变化的 effect,use(Layout)Effect 才可以根据它的依赖变化情况来决定是否执行回调。

function schedulePassiveEffects(finishedWork: Fiber) {
// 获取到函数组件的 updateQueue
const updateQueue = finishedWork.updateQueue;
// 获取 effect 链表
const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
if (lastEffect !== null) {
const firstEffect = lastEffect.next;
let effect = firstEffect;
// 循环 effect 链表
do {
const { next, tag } = effect;
if (
(tag & HookPassive) !== NoHookEffect &&
(tag & HookHasEffect) !== NoHookEffect
) {
// 当 effect 的 tag 含有 HookPassive 和 HookHasEffect 时
// 分别插入销毁函数数组和创建函数数组
enqueuePendingPassiveHookEffectUnmount(finishedWork, effect);
enqueuePendingPassiveHookEffectMount(finishedWork, effect);
}
effect = next;
} while (effect !== firstEffect);
}
}

上边已经填充好 effect 执行数组了,接下来就是执行这个数组。执行过程是先循环待销毁的 effect 数组,再循环待创建的 effect 数组,这一过程发生在flushPassiveEffectsImpl函数中

function flushPassiveEffectsImpl() {
// 先校验,如果root上没有 Passive effectTag 的节点,则直接return
if (rootWithPendingPassiveEffects === null) {
return false;
}
// ...
// 执行effect的销毁
const unmountEffects = pendingPassiveHookEffectsUnmount;
pendingPassiveHookEffectsUnmount = [];
for (let i = 0; i < unmountEffects.length; i += 2) {
const effect = ((unmountEffects[i]: any): HookEffect);
const fiber = ((unmountEffects[i + 1]: any): Fiber);
const destroy = effect.destroy;
effect.destroy = undefined;

if (typeof destroy === "function") {
try {
destroy();
} catch (error) {
captureCommitPhaseError(fiber, error);
}
}
}

// 再执行effect的创建
const mountEffects = pendingPassiveHookEffectsMount;
pendingPassiveHookEffectsMount = [];
for (let i = 0; i < mountEffects.length; i += 2) {
const effect = ((mountEffects[i]: any): HookEffect);
const fiber = ((mountEffects[i + 1]: any): Fiber);
try {
const create = effect.create;
effect.destroy = create();
} catch (error) {
captureCommitPhaseError(fiber, error);
}
}
// ...
return true;
}

useLayoutEffect​

useLayoutEffect 在执行的时候,也是先执行上一次的销毁函数再执行创建函数。和 useEffect 不同的是这两者都是同步执行的,前者在 mutation 阶段执行,后者在 layout 阶段执行。

与 useEffect 不同的是,它不用数组去存储销毁函数和创建函数,而是直接操作当前 fiber 上的 updateQueue

  1. 卸载上一次的 effect,发生在 mutation 阶段
// 调用卸载layout effect的函数,传入layout有关的effectTag和说明effect有变化的effectTag:HookLayout | HookHasEffect
commitHookEffectListUnmount(HookLayout | HookHasEffect, finishedWork);

function commitHookEffectListUnmount(tag: number, finishedWork: Fiber) {
const updateQueue = finishedWork.updateQueue;
const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;

// 循环updateQueue上的effect链表
if (lastEffect !== null) {
const firstEffect = lastEffect.next;
let effect = firstEffect;
do {
if ((effect.tag & tag) === tag) {
const destroy = effect.destroy;
effect.destroy = undefined;
if (destroy !== undefined) {
destroy();
}
}
effect = effect.next;
} while (effect !== firstEffect);
}
}
  1. 执行本次的 effect 创建,发生在 layout 阶段
// 调用创建layout effect的函数
commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork);

function commitHookEffectListMount(tag: number, finishedWork: Fiber) {
const updateQueue = finishedWork.updateQueue;
const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
if (lastEffect !== null) {
const firstEffect = lastEffect.next;
let effect = firstEffect;
do {
if ((effect.tag & tag) === tag) {
// 创建
const create = effect.create;
effect.destroy = create();
}
effect = effect.next;
} while (effect !== firstEffect);
}
}

一些问题​

  • useEffect 和 useLayoutEffect 的使用和区别

useEffect在实现上利用 scheduler(关于 scheduler 后续会写篇文章介绍) 的异步调度函数:scheduleCallback,这函数会将执行 useEffect 的动作作为一个任务去调度,这个任务会异步调用,发生在 DOM 渲染之后

而useLayoutEffect发生在 DOM 渲染之前,执行时机和componentDidMount、componentDidUpdate是一致的,也就是说如果在useEffect中触发状态更新或者 DOM 渲染,会额外追加一次 DOM 渲染

一般情况下建议使用 useEffect,因为它 不会阻塞渲染,useLayoutEffect 会阻塞渲染,只有在涉及到修改 DOM、动画等场景下考虑使用 useLayoutEffect

  • 不同 useEffect 回调触发了多次 setState,会触发多次 render 吗?

不会。setState 最后其实都会调用 scheduleUpdateOnFiber

// scheduleUpdateOnFiber
// ...
if (executionContext === NoContext) {
// Flush the synchronous work now, unless we're already working or inside
// a batch. This is intentionally inside scheduleUpdateOnFiber instead of
// scheduleCallbackForFiber to preserve the ability to schedule a callback
// without immediately flushing it. We only do this for user-initiated
// updates, to preserve historical behavior of legacy mode.
flushSyncCallbackQueue();
}
// ...

只有当 executionContext 为 NoContext 才会触发同步更新,其余情况都是批量更新。在 commit 的 beforeMutation 阶段,会赋值 executionContext 为 CommitContext,代表进入 commit 阶段

// beforeMutation
// 将当前上下文标记为CommitContext,作为commit阶段的标志
// ...
const prevExecutionContext = executionContext;
executionContext |= CommitContext;
// ...

· 13 min read

得闲翻下 react17 的源码和相关文章,学习一下 react 的 useState 的实现原理。整个过程蛮有趣的,首先得理解 fiber 架构,其次在 debugger 源码的时候得学习和理解一些必要的上下文,整体来说受益匪浅

前置知识​

闭包​

在 hooks 的应用比如 dispatch 函数,也就是 useState 返回的第二个参数

闭包是指有权访问另一个函数作用域中变量或方法的函数,创建闭包的方式就是在一个函数内创建闭包函数,通过闭包函数访问这个函数的局部变量, 利用闭包可以突破作用链域的特性,将函数内部的变量和方法传递到外部。

单链表​

A-->B-->C。fiber 架构通过使用这种数据结构,实现了 render 过程的可中断性。比如更新了 B 节点,突然有高优先级插入,这个时候只需要记住当前更新到的节点是 B 即可,然后去执行高优任务,之后再从 B 节点继续更新

fiber & hook​

fiber 对象和 hook 对象的关系如下图: fiber & hook

fiber 架构​

React 16 引入了 fiber 架构,会基于 ReactElement 生成唯一的 fiber 对象。在更新阶段,会基于旧的 fiber 链表创建一个新的 fiber 链表(如下图),可以复用旧的对象,并且获取旧 hooks 的状态

fiber双链表

两个单链表​

hooks 链表​

/*
Hooks are stored as a linked list on the fiber's memoizedState field.
hooks 以链表的形式存储在fiber节点的memoizedState属性上
The current hook list is the list that belongs to the current fiber.
当前的hook链表就是当前正在遍历的fiber节点上的
The work-in-progress hook list is a new list that will be added to the work-in-progress fiber.
work-in-progress hook 就是即将被添加到正在遍历fiber节点的hooks新链表
*/
let currentHook: Hook | null = null;
let nextCurrentHook: Hook | null = null;

无论是初次挂载还是更新,每调用一次 hooks 函数,都会产生一个 hook 对象与之对应,hook 对象结构如下

{
baseQueue: null, // 当前 update
baseState: 'hook1', // 初始值,即 useState 入参
memoizedState: null, // 当前状态(更新时表示上一次的状态)
queue: null, // 待执行的更新队列(queue.pending)
next: { // 下一个 hook
baseQueue: null,
baseState: null,
memoizedState: 'hook2',
next: null
queue: null
}
}

产生的 hook 对象依次排列,形成链表存储到函数组件 fiber.memoizedState 上。在这个过程中,有一个十分重要的指针:workInProgressHook,它通过记录当前生成(更新)的 hook 对象,可以间接反映在组件中当前调用到哪个 hook 函数了。每调用一次 hook 函数,就将这个指针的指向移到该 hook 函数产生的 hook 对象上

比如先调用 hookA

fiber.memoizedState: hookA
^
workInProgressHook

调用 hookB

fiber.memoizedState: hookA -> hookB
^
workInProgressHook

hooks 的更新队列​

这其实是个单链表环,存放某个 hook 的更新队列,如下图,是一个 useState 的更新队列:

任务待执行链表.png

其实还有一个副作用链表,跟 useEffect 有关,最终会挂到 root 节点上

源码执行流程​

renderWithHooks​

对于函数组件(FunctionComponent)类型,在 beginwork 的时候会调用 renderWithHooks 注册,根据 mount 还是 update 调用一个函数生成 hooks 链表挂在 Fiber 上

// renderWithHooks
function renderWithHooks(current, workInProgress, Component, props, secondArg, nextRenderLanes) {
...
// 当前fiber
currentlyRenderingFiber$1 = workInProgress;
// hooks队列
workInProgress.memoizedState = null;
// 副作用队列
workInProgress.updateQueue = null;
{
if (current !== null && current.memoizedState !== null) {
ReactCurrentDispatcher$1.current = HooksDispatcherOnUpdateInDEV;
} else if (hookTypesDev !== null) {
ReactCurrentDispatcher$1.current = HooksDispatcherOnMountWithHookTypesInDEV;
} else {
ReactCurrentDispatcher$1.current = HooksDispatcherOnMountInDEV;
}
}
...
return children;
}

mount​

usestate源码执行顺序mount.png

update​

usestate源码执行顺序.png

mount 阶段​

/**
* ReactCurrentDispatcher 是一个内部储存状态的状态机.
* 主要作用还是用于切换不同执行时机的 dispatcher 对象
* */
const ReactCurrentDispatcher = { current: null };
function resolveDispatcher() {
const dispatcher = ReactCurrentDispatcher.current;
return dispatcher;
}
function useState(initialState) {
var dispatcher = resolveDispatcher();
return dispatcher.useState(initialState);
}
// resolveDispatcher
...
useState: function (initialState) {
...
try {
return mountState(initialState);
} finally {
ReactCurrentDispatcher$1.current = prevDispatcher;
}
},
...
// mountWorkInProgressHook
function mountWorkInProgressHook() {
var hook = {
memoizedState: null,
baseState: null,
baseQueue: null,
queue: null,
next: null,
};

if (workInProgressHook === null) {
currentlyRenderingFiber$1.memoizedState = workInProgressHook = hook;
} else {
workInProgressHook = workInProgressHook.next = hook;
}

return workInProgressHook;
}

// mountState
function mountState(initialState) {
// 创建 hook 对象
var hook = mountWorkInProgressHook();

if (typeof initialState === "function") {
initialState = initialState();
}

hook.memoizedState = hook.baseState = initialState;

var queue = (hook.queue = {
pending: null, // update 队列
dispatch: null, // 触发函数,会触发更新,useState返回值的第二个参数
lastRenderedReducer: basicStateReducer, // 最后一次使用的 reducer,初始化时使用最基础的 reducer
lastRenderedState: initialState, // 上一次的值,挂载阶段该值为初始值
});
var dispatch = (queue.dispatch = dispatchAction.bind(
null,
currentlyRenderingFiber$1,
queue
));
// dispatch是一个闭包函数
return [hook.memoizedState, dispatch];
}

useState 默认使用的更新器是 basicStateReducer

function basicStateReducer(state, action) {
return typeof action === "function" ? action(state) : action;
}

update 阶段​

先执行 useState 返回的 dispatch 函数

// mountState
...
var dispatch = queue.dispatch = dispatchAction.bind(null, currentlyRenderingFiber$1, queue);
return [hook.memoizedState, dispatch];
function dispatchAction(fiber, queue, action) {
...
// 更新对象,是一个链表节点
var update = {
lane: lane, // 优先级
action: action, // 新值,也就是 setState 传入的参数
next: null, // 指向下一个更新对象
eagerReducer: null, // 上一次使用的 reducer
eagerState: null, // 旧值
};

var pending = queue.pending;
if (pending === null) {
// 在队列上添加当前hook的首个更新对象,并保持循环
update.next = update;
} else {
// 非首个更新对象,会追加到更新队列之后
update.next = pending.next;
pending.next = update;
}
queue.pending = update;

// fiber.alternate 指向旧 fiber 链表
var alternate = fiber.alternate;

if (fiber === currentlyRenderingFiber$1 || alternate !== null && alternate === currentlyRenderingFiber$1) {
didScheduleRenderPhaseUpdateDuringThisPass = didScheduleRenderPhaseUpdate = true;
} else {
if (fiber.lanes === NoLanes && (alternate === null || alternate.lanes === NoLanes)) {
var lastRenderedReducer = queue.lastRenderedReducer;

if (lastRenderedReducer !== null) {
var prevDispatcher;

{
// NOTE 暂不清楚这一步的作用
prevDispatcher = ReactCurrentDispatcher$1.current;
ReactCurrentDispatcher$1.current = InvalidNestedHooksDispatcherOnUpdateInDEV;
}

try {
var currentState = queue.lastRenderedState;
// 计算新的 state
var eagerState = lastRenderedReducer(currentState, action);

update.eagerReducer = lastRenderedReducer;
update.eagerState = eagerState;

if (objectIs(eagerState, currentState)) {
// 前后值没变,没必要更新
return;
}
} catch (error) {
// Suppress the error. It will throw again in the render phase.
} finally {
{
ReactCurrentDispatcher$1.current = prevDispatcher;
}
}
}
}
...
// 发起一次调度更新
scheduleUpdateOnFiber(fiber, lane, eventTime);
}
}

更新 WorkInProgressHook

function updateWorkInProgressHook() {
var nextCurrentHook;

if (currentHook === null) {
var current = currentlyRenderingFiber$1.alternate;

if (current !== null) {
nextCurrentHook = current.memoizedState;
} else {
nextCurrentHook = null;
}
} else {
nextCurrentHook = currentHook.next;
}

var nextWorkInProgressHook;

if (workInProgressHook === null) {
nextWorkInProgressHook = currentlyRenderingFiber$1.memoizedState;
} else {
nextWorkInProgressHook = workInProgressHook.next;
}

if (nextWorkInProgressHook !== null) {
workInProgressHook = nextWorkInProgressHook;
nextWorkInProgressHook = workInProgressHook.next;
currentHook = nextCurrentHook;
} else {
...
currentHook = nextCurrentHook;
var newHook = {
memoizedState: currentHook.memoizedState,
baseState: currentHook.baseState,
baseQueue: currentHook.baseQueue,
queue: currentHook.queue,
next: null
};

if (workInProgressHook === null) {
currentlyRenderingFiber$1.memoizedState = workInProgressHook = newHook;
} else {
workInProgressHook = workInProgressHook.next = newHook;
}
}

return workInProgressHook;
}

真正得到最终状态,其实是在下一次获取状态的时候。

更新阶段开始时,同样会执行 renderWithHooks,此时会将 ReactCurrentDispatcher.current 指向 HooksDispatcherOnUpdate 对象

const HooksDispatcherOnUpdate: Dispatcher = {
...
useState: updateState,
}
function updateState(initialState) {
return updateReducer(basicStateReducer, initialState);
}
...
// updateReducer
function updateReducer(reducer, initialArg, init) {
var hook = updateWorkInProgressHook();
var queue = hook.queue;
...
do {
...
if (update.eagerReducer === reducer) {
newState = update.eagerState;
} else {
// 遍历更新队列,得到最新值
var action = update.action;
newState = reducer(newState, action);
}
...
update = update.next;
} while (update !== null && update !== first);
...
var dispatch = queue.dispatch;
return [hook.memoizedState, dispatch];
}

简单总结一下 update 的过程:

  1. 调用 dispatcher 函数
  2. 收集 update,将 update 对象按序插入更新队列 queue.pending
  3. 调度一次 React 的更新
  4. 重新执行组件函数时,useState 会被重新执行,在 resolve dispatcher 的阶段取到负责更新的 dispatcher
  5. 按序执行更新队列,拿到最新的 state
  6. 渲染真实 DOM,更新结束

其他问题​

为什么在 React 16 前,函数式组件不能拥有状态管理?

因为 16 以前只有类组件在更新时存在实例,而 16 以后 Fiber 架构的出现,让每一个节点都拥有对应的实例,也就拥有了保存状态的能力

为什么只能在函数组件中使用 hooks?

只有函数组件才走 renderWithHooks 的逻辑

为什么 hooks 不能在循环、判断语句中调用,而只能在函数最外层使用?

因为在更新时,这个队列需要是一致的,才能保证 hooks 的结果正确。

useState 的 setState 是同步操作还是异步操作?

默认是异步。从上面的源码分析可以看出,执行 setState 时会将更新对象(update)存入更新队列,等到 commit 阶段才会一次性执行该更新队列。在没有重新执行 app 函数时,拿到的始终是旧状态,所以就造成了异步的现象

连续执行同一个 setState,会造成多次渲染吗?比如下面这段代码:

onClick = () => {
setState(0);
setState(1);
setState(2);
};

不会。执行事件函数时,会判断当前是否处于批量更新(具体逻辑可以参见 scheduleUpdateOnFiber 源码),如果处于批量更新状态,说明还不能重新渲染,需要等待该状态结束(比如 react 中的 onClick 事件)。在批量更新阶段,每个 hooks 会通过 queue 将更新(update)存入更新队列(单链表)中,这个队列会保证更新顺序。等到批量更新状态结束了,才会重新执行组件函数,之后执行到对应 hook 时,会一次性执行挂载在 hook 上的更新队列,从而更新状态。

执行两次 setState(0) 会执行两次函数组件吗?

不会。在更新时会通过 ObjectIs 判断更新前后的值是否变化,如果没变化是不会调度更新的

· 17 min read

在 react 16 之前,更新虚拟 DOM 为 stack reconciler,这是一个递归的过程,在树很深的时候,单次 diff 时间过长会造成 JS 线程持续被占用,用户交互响应迟滞,页面渲染会出现明显的卡顿。为了解决这种问题,react 团队使用 fiber 架构重构了 React,使其能够将任务分片,划分优先级,同时能够实现类似于操作系统中对线程的抢占式调度

写在前面​

浏览器每一帧都需要完成哪些工作? 浏览器一帧内的工作.png

通过上图可看到,一帧内需要完成如下六个步骤的任务:

  1. 处理用户交互
  2. 解析 js 脚本
  3. Begin frame。resize、scroll 等事件的处理
  4. rAF
  5. 布局
  6. 绘制

页面是一帧一帧绘制出来的,当 FPS(每秒绘制的帧数)达到 60 时,页面是流畅的,小于这个值时,用户会感觉到卡顿。也就是说浏览器 1s 绘制 60 帧,每一帧分到的时间是 1000/60 ≈ 16 ms。所以我们应该力求不让一帧的工作量超过 16ms。

由此我们知道,浏览器是一帧一帧执行的,在两个执行帧之间,主线程通常会有一小段空闲时间

浏览器一帧工作的空余.png

requestIdleCallback 可以在这个空闲期(Idle Period)调用空闲期回调(Idle Callback),执行一些任务

Fiber reconciler​

Fiber 协调核心:可中断、可恢复 和 优先级

新特性如下:

  • 增量式渲染。把渲染任务拆分成块,匀到多帧
  • 能够暂停、终止和复用渲染任务
  • 给不同类型的更新赋予优先级
  • 并行渲染(还在开发中,值得期待)

将以前的 stack reconciler 拆分成两个阶段:render 与 commit。render 阶段是可打断的,被拆分成一个个的小任务(fiber),在每一侦的渲染空闲期执行。然后是 commit 阶段,这个阶段是不拆分且不能打断的,将最终的 effectList 一口气更新到页面上。这两个阶段后续会详细介绍

  • 每个更新任务都会赋予一个优先级
  • 当更新任务抵达调度器时,高优先级的更新任务(A)会更快地被调度进入 Reconciler —— 优先级
  • 此时有新的更新任务(B),调度器会检查它优先级,若高于当前任务(A),处于当前 Reconciler 层的 A 任务会被中断,调度器将 B 任务推入 Reconciler 层 —— 可中断
  • 当 B 任务完成渲染后,新一轮调度开始,之前被中断的 A 任务将会被重新推入 Reconciler 层,继续它的渲染 —— 可恢复

fiber 对象​

React Fiber 把更新过程碎片化,执行过程如下面的图所示,每执行完一段更新过程,就把控制权交还给 React 负责任务协调的模块,看看有没有其他紧急任务要做,如果没有就继续去更新,如果有紧急任务,那就去做紧急任务。

维护每一个分片的数据结构,就是 Fiber。

有了分片之后,更新过程的调用栈如下图所示,中间每一个波谷代表深入某个分片的执行过程,每个波峰就是一个分片执行结束交还控制权的时机。让线程处理别的事情

fiber调度.png

一个 react 节点对应着一个 fiber 节点

type Fiber = {
// 标识不同的组件类型
// FunctionComponent: 0; ClassComponent: 1...
tag: WorkTag,

key: null | string,

// react 元素类型
// 可以为ClassComponent、FunctionComponent、Symbol、HostComponent...
elementType: any,

// The resolved function/class/ associated with this fiber.
type: any,

// 该fiber节点对应的ReactElement对象
stateNode: any,

// fiber的父级
return: Fiber | null,

// 单链表树状结构
child: Fiber | null,
sibling: Fiber | null,
index: number,

ref: null | (((handle: mixed) => void) & { _stringRef: ?string }) | RefObject,

// 新 props
pendingProps: any,
// 旧 props
memoizedProps: any,

// 更新队列
updateQueue: UpdateQueue<any> | null,

// 旧 state
memoizedState: any,

// DOM diff相关
effectTag: SideEffectTag,

// 副作用链表
nextEffect: Fiber | null,
firstEffect: Fiber | null,
lastEffect: Fiber | null,

// 标识该组件应该在未来的某个时刻完成更新
// expirationTime 五种类型:
// NoWork(默认0)、Never(1)、Interactive(100 - 150)、Async(250 - 5000)、Sync(Number.MAXINTEGER)
expirationTime: ExpirationTime,

// 快速确定子fiber树有没有挂起的更改
childExpirationTime: ExpirationTime,

// fiber 的镜像节点
alternate: Fiber | null,
};

fiber 树示意图如下:

fiber树结构.png

演示:

创建fiber.gif

双链表​

在 react 中始终存在 workInprogressTree(future vdom) 与 oldTree(current vdom)两个链表,两个链表相互引用。

fiber双链表

双链表的好处有:

  • 复用内部对象(fiber),可以节省内存分配、GC 的时间开销
  • 获取旧状态
  • 当 workInprogressTree 生成报错时,这时也不会导致页面渲染崩溃,而只是更新失败,页面仍然还在

三个阶段​

Scheduler​

该阶段主要是给任务分配优先级,统筹任务调度

Scheduler 的源码解析可以参考:https://juejin.cn/post/6889314677528985614

react 16 基于 requestIdleCallback 的 polyfill 方案来实现任务调度,用法如下:

window.requestIdleCallback(callback[, options])
let handle = window.requestIdleCallback((idleDeadline) => {
const {didTimeout, timeRemaining} = idleDeadline;
console.log(`是否超时?${didTimeout}`);
console.log(`可用时间剩余${timeRemaining.call(idleDeadline)}ms`);
// do sth
const now = Date.now(), timespent = 10;
while (Date.now() < now + timespent);
console.log(`花了${timespent}ms`);
console.log(`可用时间剩余${timeRemaining.call(idleDeadline)}ms`);
}, {timeout: 1000});
// 是否超时?false
// 可用时间剩余45ms
// 花了12ms
// 可用时间剩余32ms

由于 requestIdleCallback 有兼容问题,react 团队采用的是它的 polyfill 方案,可参考

Render​

遍历 Fiber 树,得出需要更新的节点信息。可以被打断,让位于优先级更高的操作,比如用户点击等

  1. 从Fiber Root开始遍历,构建一个新的 Fiber 树。performSyncWorkOnRoot(root)-->renderRootSync
  2. 更新每个 fiber。workLoopSync-->performUnitOfWork-->beginWork
function workLoopSync() {
// workInProgress:当前正在处理的节点
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
}

function performUnitOfWork(unitOfWork: Fiber): void {
const current = unitOfWork.alternate;
// ...
// NOTE
next = beginWork(current, unitOfWork, subtreeRenderLanes);
// ...
if (next === null) {
// 如果没有子节点,则完成当前工作
completeUnitOfWork(unitOfWork);
} else {
workInProgress = next;
}
ReactCurrentOwner.current = null;
}

beginWork 主要做了以下事情:

  1. 判断 fiber 节点是否可以复用
  2. 根据不同的 Tag(标记不同的组件类型:纯组件、函数组件、类组件),生成不同的 fiber 节点赋值给workInprogress.child
function beginWork(current, workInProgress, renderLanes) {
// ...
switch (workInProgress.tag) {
// ...
case FunctionComponent: {
const Component = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
const resolvedProps =
workInProgress.elementType === Component
? unresolvedProps
: resolveDefaultProps(Component, unresolvedProps);

// NOTE
return updateFunctionComponent(
current,
workInProgress,
Component,
resolvedProps,
renderLanes,
);
}
case ClassComponent: {
const Component = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
const resolvedProps =
workInProgress.elementType === Component
? unresolvedProps
: resolveDefaultProps(Component, unresolvedProps);
// 执行 render()、生命周期钩子函数等 -> reconcileChildren
return updateClassComponent(
current,
workInProgress,
Component,
resolvedProps,
renderLanes,
);
}
case HostRoot:
// ReactDOM.render(<App/>) -> reconcileChildren
return updateHostRoot(current, workInProgress, renderLanes);
// ...
}

以updateFunctionComponent为例,主要干了几件事:

  1. 生成新的 hooks 链表并挂载在 fiber 节点上
  2. 调用 reconcileChildren
  3. 返回 workInprogress.child
function updateFunctionComponent(
current,
workInProgress,
Component,
nextProps: any,
renderLanes
) {
// ...
{
nextChildren = renderWithHooks(
current,
workInProgress,
Component,
nextProps,
context,
renderLanes
);
}
// ...
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
return workInProgress.child;
}
// ...

reconcileChildren主要是处理子 fiber 节点,在这个阶段做 diff,并在变更的节点上标记副作用类型

// 处理子 fiber 节点
export function reconcileChildren(
current: Fiber | null,
workInProgress: Fiber,
nextChildren: any,
renderLanes: Lanes,
) {
if (current === null) {
workInProgress.child = mountChildFibers(
workInProgress,
null,
nextChildren,
renderLanes,
);
} else {
workInProgress.child = reconcileChildFibers(
workInProgress,
current.child,
nextChildren,
renderLanes,
);
}
}
// ...
function reconcileChildFibers(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChild: any,
lanes: Lanes,
): Fiber | null {
const isUnkeyedTopLevelFragment = ...;
// Handle object types
const isObject = typeof newChild === 'object' && newChild !== null;
if (isObject) {
// 根据不同的类型,处理不同的节点对比
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE:
return placeSingleChild(
reconcileSingleElement(
returnFiber,
currentFirstChild,
newChild,
lanes,
),
);
...
}

if (typeof newChild === 'string' || typeof newChild === 'number') {
return placeSingleChild(
reconcileSingleTextNode(
returnFiber,
currentFirstChild,
'' + newChild,
lanes,
),
);
}
// 多节点数组
if (isArray(newChild)) {
return reconcileChildrenArray(
returnFiber,
currentFirstChild,
newChild,
lanes,
);
}
...
}
...
// reconcileChildFibers 会判断节点变更的类型,赋值flags
// 在commit阶段,根据flags更新dom
// placeSingleChild 会先判断是否是第一次渲染,是的话会增加Placement副作用
function placeSingleChild(newFiber: Fiber): Fiber {
if (shouldTrackSideEffects && newFiber.alternate === null) {
newFiber.flags = Placement;
}
return newFiber;
}
...
// 对于 ReactElement,会调用 reconcileSingleElement
function reconcileSingleElement(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
element: ReactElement,
expirationTime: ExpirationTime,
): Fiber {
// 新 key
const key = element.key;
// 旧的第一个子节点
let child = currentFirstChild;

while (child !== null) {
// 新旧key相同
if (child.key === key) {
// 节点类型未改变
if (
child.tag === Fragment
? element.type === REACT_FRAGMENT_TYPE
: child.elementType === element.type ||
(__DEV__
? isCompatibleFamilyForHotReloading(child, element)
: false)
) {
// 复用当前 child,先删除它的兄弟节点
deleteRemainingChildren(returnFiber, child.sibling);
// 复制 fiber 节点,重置 index 和 sibling
const existing = useFiber(
child,
element.type === REACT_FRAGMENT_TYPE
? element.props.children
: element.props,
expirationTime,
);
// ...
// 设置父节点
existing.return = returnFiber;
return existing;
} else {
// 节点类型不同,要删除旧节点
deleteRemainingChildren(returnFiber, child);
break;
}
} else {
// 新旧 key 不相同,直接删除
deleteChild(returnFiber, child);
}
child = child.sibling;
}

// child 为 null,需要建立子节点
if (element.type === REACT_FRAGMENT_TYPE) {
// 创建 Fragment 类型的 fiber 节点
const created = createFiberFromFragment(
element.props.children,
returnFiber.mode,
expirationTime,
element.key,
);
created.return = returnFiber;
return created;
} else {
// 创建 Element 类型的 fiber 节点
const created = createFiberFromElement(
element,
returnFiber.mode,
expirationTime,
);
created.ref = coerceRef(returnFiber, currentFirstChild, element);
created.return = returnFiber;
return created;
}
}

...
// 副作用类型
import {
NoEffect,
PerformedWork,
Placement, // 挂载,didMount
Update, // 更新, didUpdate
Snapshot, // getSnapshotBeforeUpdate,更新之前设置快照
PlacementAndUpdate,
Deletion, // 卸载,willUnmount
ContentReset,
Callback,
DidCapture,
Ref,
Incomplete,
HostEffectMask,
Passive,
} from'shared/ReactSideEffectTags';
  1. fiber 节点更新收尾,串联 fiber 节点,收集 effect(如果有副作用)。completeUnitOfWork、completeWork
function completeUnitOfWork(unitOfWork: Fiber) {
let completedWork = unitOfWork;
do {
const current = completedWork.alternate;
const returnFiber = completedWork.return;
if ((completedWork.flags & Incomplete) === NoFlags) {
let next;
// 处理 fiber 的 props、创建 dom 对象、绑定事件等
next = completeWork(current, completedWork, subtreeRenderLanes);
// ...
// 收集 effect,把子节点 side Effect 加到父节点的 side Effect 上
if (
returnFiber !== null &&
(returnFiber.flags & Incomplete) === NoFlags
) {
if (returnFiber.firstEffect === null) {
returnFiber.firstEffect = completedWork.firstEffect;
}
if (completedWork.lastEffect !== null) {
if (returnFiber.lastEffect !== null) {
returnFiber.lastEffect.nextEffect = completedWork.firstEffect;
}
returnFiber.lastEffect = completedWork.lastEffect;
}

const flags = completedWork.flags;
if (flags > PerformedWork) {
if (returnFiber.lastEffect !== null) {
returnFiber.lastEffect.nextEffect = completedWork;
} else {
returnFiber.firstEffect = completedWork;
}
returnFiber.lastEffect = completedWork;
}
}
} else {
// ...
}
// 存在兄弟节点,将 workInProgress 指向兄弟节点,并return,执行兄弟节点的beginWork -> Fiber Node
const siblingFiber = completedWork.sibling;
if (siblingFiber !== null) {
workInProgress = siblingFiber;
return;
}
// 没有兄弟节点,返回父节点
completedWork = returnFiber;
workInProgress = completedWork;
} while (completedWork !== null);
// ...
}

收集 effect 时,会创建 effectList,如下图所示,橙色节点上有 effect

commit​

执行到 commitRoot 时,意味着 render 阶段结束,进入 commit 阶段。该阶段主要是处理 FiberRoot 上收集的 effectList,只针对变化的节点做工作,并执行生命周期钩子函数

先看下几个生命周期的执行时机:

// 第1阶段 render/reconciliation
componentWillMount;
componentWillReceiveProps;
shouldComponentUpdate;
componentWillUpdate;

// 第2阶段 commit
componentDidMount;
componentDidUpdate;
componentWillUnmount;

第 1 阶段的生命周期函数可能会被多次调用,默认以低优先级执行,被高优先级任务打断后,稍后会重新执行

commitRoot 主要是执行commitRootImpl这个函数

function commitRoot(root) {
// ...
commitRootImpl.bind(null, root, renderPriorityLevel)
// ...
}
// ...
function commitRootImpl() {
do {
// NOTE 调用 flushPassiveEffects 执行完所有 effect
flushPassiveEffects();
} while (rootWithPendingPassiveEffects !== null);
// ...
firstEffect = finishedWork.firstEffect;
if (firseEffect !== null) {
nextEffect = firstEffect;
// 第一阶段,before mutation
do {
commitBeforeMutationEffects();
} while(nextEffect !== null)
// ...
// 重置 nextEffect 为 firstEffect,接着进行第二阶段
nextEffect = firstEffect;
// 第二阶段 mutation
do {
commitMutationEffects(root, renderPriorityLevel);
} while(nextEffect !== null)

// 将当前的 workInProgress树 作为 current 树
root.current = finishedWork;

// ...
// 第三阶段 layout
do {
commitLayoutEffects(root, expirationTime);
} while(nextEffect)
// ...
// 确保 root 上所有的 work 都被调度完
ensureRootIsScheduled(root);
// 检测在 useLayoutEffect 中是否做了布局修改等,刷新布局,如果在 layoutEffect 中调用了 setState 也会在该函数中检测中并开启新的一轮调度
flushSyncCallbackQueue();
}
...
}

commit 阶段可以细分为以下三个阶段:

  1. before mutation:读取组件变更前的状态,对于类组件,会调用生命周期函数getSnapshotBeforeUpdate,在 DOM 变更前可以获取到组件实例相关的信息;对于函数组件,会异步调度 useEffect
function commitBeforeMutationEffects() {
while (nextEffect !== null) {
const effectTag = nextEffect.effectTag;
// 对于使用 getSnapShowBeforeUpdate 的组件 fiber.effectTag |= SnapShot
if ((effectTag & Snapshot) !== NoEffect) {
// ...
const current = nextEffect.alternate;
// 执行 getSnapShotBeforeUpdate 生命周期
commitBeforeMutationEffectOnFiber(current, nextEffect);
// ...
}
// 对于使用 useEffect 的组件,其 Fiber.effectTag = UpdateEffect | PassiveEffect
if ((effectTag & Passive) !== NoEffect) {
// If there are passive effects, schedule a callback to flush at
// the earliest opportunity.
if (!rootDoesHavePassiveEffects) {
rootDoesHavePassiveEffects = true;
// NOTE
scheduleCallback(NormalPriority, () => {
flushPassiveEffects();
return null;
});
}
}
nextEffect = nextEffect.nextEffect;
}
}
  1. mutation:根据 effectTag 执行对应的 dom 操作。对于类组件,还会调用 componentWillUnmount;对于函数组件,还会执行 useLayoutEffect 的销毁函数
function commitMutationEffects(root: FiberRoot, renderPriorityLevel) {
while (nextEffct) {
// ...
const effectTag = nextEffect.effectTag;
let primaryEffectTag =
effectTag & (Placement | Update | Deletion | Hydrating);
// ...
switch (primaryEffectTag) {
// 挂载 DOM
case Placement: {
commitPlacement(nextEffect);
nextEffect.effectTag &= ~Placement;
return;
}
// 更新组件及DOM
case PlacementAndUpdate: {
commitPlacement(nextEffect);
nextEffect.effectTag &= ~Placement;
// 刷新 layoutEffect.destroy
const current = nextEffect.alternate;
commitWork(current, nextEffect);
break;
}
// 更新组件
case Update: {
const current = nextEffect.alternate;
commitWork(current, nextEffect);
break;
}
// 卸载
case Deletion: {
commitDeletion(root, nextEffect, renderPriorityLevel);
break;
}
// ...
}
nextEffect = nextEffect.nextEffect;
}
}
  1. layout:在 DOM 操作完成后,读取组件的状态,对于类组件,会调用生命周期componentDidMount和componentDidUpdate;对于函数式组件,主要执行 commitHookEffectListMount
function commitLayoutEffects(root, committedExpirationTime) {
while (nextEffect !== null) {
// ...
const effectTag = nextEffect.effectTag;
if (effectTag & (Update | Callback)) {
recordEffect();
const current = nextEffect.alternate;
// didmount
commitLayoutEffectOnFiber(
root,
current,
nextEffect,
committedExpirationTime
);
}
// ...
}
}

function commitLayoutEffectOnFiber(
finishedRoot,
current,
finishedWork,
committedExpirationTime
) {
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case SimpleMemoComponent:
case Block: {
commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork);
// ...
return;
}
case ClassComponent: {
// didMount
}
}
}