Skip to main content

6 posts tagged with "react"

View All Tags

· One min read

最近在一个 react 单页面项目(webpack4)里遇到了一个热更新的报错: Uncaught ReferenceError: process is not defined。

原因是 react-error-overlay 这个插件版本(@6.0.11)太高,这个版本是支持了 webpack5,和 react-scripts@4 有不兼容的依赖项导致报错

解决办法是安装 6.0.9 版本

yarn add react-error-overlay@6.0.9

调整 package.json

{
"resolutions": {
"react-error-overlay": "6.0.9"
},
"overrides": {
"react-error-overlay": "6.0.9"
}
}

resolutions for yarn,overrides for npm >= 8.3

如果是用的 npm,并且 6<=版本<8.3,可以用以下方法

npm install npm-force-resolutions --save-dev

调整 package.json

{
"resolutions": {
"react-error-overlay": "6.0.9"
},
"scripts": {
"preinstall": "npm-force-resolutions"
}
}

后续执行 npm install 就会强制 react-error-overlay 为 6.0.9

by the way,升级 webpack5 也不是不行 ~ 只不过坑也蛮多的。上述办法就比较轻松了

· 4 min read

写在前面

  • 技术赋能业务,业务促进技术
  • 不要提前设计,不要为了用技术而用技术

拥有自己的应用模板

  • 沉淀自己的应用模板很重要,这是属于你的最佳实践
    • 快速新建项目
    • 代码规范配置
    • 预设高质量依赖
    • 良好的构建配置和插件集合
  • 尝试给自己写个脚手架,和模板配合

ts + eslint

  • 最大限度确保代码的质量,不过也不能滥用 any
  • 封装公共 type,比如 TPlainObjectISOTimeStamp、颜色枚举等
  • eslint 只做代码检查,不做代码格式化

prettier

  • 代码格式化一致性,再也不怕帮同事改代码了
  • commitlint 代码提交前强制格式化
  • .vscode 增加 formatOnSave

key

请给遍历组件加上 key,提高组件的复用率,避免错位(index 做 key)导致的更新异常现象

正确用 Hook

  • 统一使用 Hook 编写组件
  • 只能在最顶层使用,不要在循环、条件语句或函数中使用
  • 变量和对应的函数(func/effect)放一起,而不是给所有变量和所函数划分界限
  • 适当使用 useState,部分情况可以用 useRef 替代

最小原则

  • 最小渲染次数。这个是架构层面出发的必须注重的优化,react 应用的性能优化基本都会从这里开始。通过 useMemo 或者 shouldUpdateComponent/memo 来避免组件的重复渲染
  • 擅用缓存。可以借助 useCallbackuseRef 缓存变量和函数,避免重复创建
  • 最少依赖项。使用新的依赖之前,先思考必要性。比如 Redux,如果只是小项目,通信也不复杂,那直接用 Context 有何不可

组件化

遵守单一职责原则,确保组件简洁、耦合度小、功能明确

能否做好组件化是鉴别前端工程师能力的一个重要方式

卸载

别忘了卸载定时器、移除事件,不要浪费内存

错误边界

给组件都统一添加错误边界(Error Boundary)来防止白屏。让 PM、测试看到白屏和局部错误是俩回事

单元测试

公共组件和工具函数/自定义 Hook,有时间的话写点单元测试,受益匪浅哦。覆盖率尽可能大于 70%,单元测试工具推荐 Jest

应用优化

  • 加快首屏
    • 组件/路由/资源懒加载
    • 本地/离线缓存
  • 减小包体积
    • 代码分割
    • 按需加载
    • tree shaking

总结

有讲的不对、需要改进的地方,欢迎评论,一块进步 ~

· 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);
}
}

一些问题

  • useEffectuseLayoutEffect 的使用和区别

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

useLayoutEffect发生在 DOM 渲染之前,执行时机和componentDidMountcomponentDidUpdate是一致的,也就是说如果在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();
}
// ...

只有当 executionContextNoContext 才会触发同步更新,其余情况都是批量更新。在 commit 的 beforeMutation 阶段,会赋值 executionContextCommitContext,代表进入 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 判断更新前后的值是否变化,如果没变化是不会调度更新的

· 12 min read

在 react 中可以通过 setState 来更新状态,重新渲染该组件及其子组件。react 15 在一次事务更新中,会合并多次状态更新(批量更新),等到事务结束再统一更新状态;而 react 16 中已经没有了事务的概念,在 Fiber 架构下,setState 是如何实现的呢?

写在前面

先看道面试题

// ...
constructor() {
super();
this.state = {
val: 0
};
}

componentDidMount() {
this.setState({val: this.state.val + 1});
console.log(this.state.val);
this.setState({val: this.state.val + 1});
console.log(this.state.val);
setTimeout(() => {
this.setState({val: this.state.val + 1});
console.log(this.state.val);
this.setState({val: this.state.val + 1});
console.log(this.state.val);
}, 0);
}
// ...
};

上面代码的执行结果是

0
0
2
3

直观点看,在 setTimeout 外面的状态更新是异步的,而 setTimeout 里的状态更新是同步的。说明 setState 更新状态并不总是异步的。当它处于 react 的生命周期钩子函数、合成事件,表现为异步;其他情况则表现为同步,比如 setTimeout 和原生事件等

本文会结合源码分析,总结下 react 15 和 16 两个版本关于 setState 的实现

react 15

react 15 是通过事务机制和锁来控制批量更新时机,实现“异步”更新

关键词:

  • Transaction(事务)
  • dirtyComponents
  • isBatchingUpdates(锁)

Transaction

看下官方关于 Transaction 的注释:

这个注释其实就是 Transaction 的核心功能,主要是包装要执行的函数 method(生命周期钩子函数或者 react 事件处理函数等),通过 Transaction.perform 来执行 method

下面是一个基础的 Transaction 对象,是 react 事务对象的父类

var Transaction = {
// 重置 Transaction,刷新之前的数据
reinitializeTransaction: function () {
// getTransactionWrappers 其实是一个获取wrapper的抽象方法,需要具体实现
// 用于获取当前事务需要的所有 wrappers
this.transactionWrappers = this.getTransactionWrappers();
if (this.wrapperInitData) {
this.wrapperInitData.length = 0;
} else {
this.wrapperInitData = [];
}
this._isInTransaction = false;
},

_isInTransaction: false,

getTransactionWrappers: null,

// 判断当前事务是否正在执行
// 可用于防止当前事务被打断
isInTransaction: function () {
return !!this._isInTransaction;
},

perform: () => {
// this._isInTransaction = true 表明正处于事务中
// this.initializeAll
// 执行method
// this.closeAll
// this._isInTransaction = false
},
initializeAll: () => {
// 遍历所有wrappers,执行initialize方法
}
closeAll: () => {
// 遍历所有wrappers,执行所有close方法
}

Transaction 执行 perform 前,会先执行所有已注入的 wrapperinitialize 方法来初始化事务,执行完方法后再调用 wrapperclose 方法来结束事务

setState

setState 的执行流程如下:

看下源码:

ReactComponent.prototype.setState = function(partialState, callback) {
this.updater.enqueueSetState(this, partialState);
if (callback) {
this.updater.enqueueCallback(this, callback, 'setState');
}
};

// ...
enqueueSetState: function(publicInstance, partialState) {
var internalInstance = getInternalInstanceReadyForUpdate(
publicInstance,
'setState',
);

var queue =
internalInstance._pendingStateQueue ||
(internalInstance._pendingStateQueue = []);
queue.push(partialState);

enqueueUpdate(internalInstance);
},
// ...

function enqueueUpdate(internalInstance) {
ReactUpdates.enqueueUpdate(internalInstance);
}
// ...
function enqueueUpdate(component) {
// ...
if (!batchingStrategy.isBatchingUpdates) {
batchingStrategy.batchedUpdates(enqueueUpdate, component);
return;
}
dirtyComponents.push(component);
}

假设现在触发了一次 react 的点击事件,并执行 setState ,此时 setState 处于 react 的更新事务流程中。开始执行事务时会设置 isBatchingUpdates 为 true,表示当前事务处于批量更新过程(锁住这个状态),导致后续加入的 setState 只会加入 dirtyComponents 中,等到事务 close 后,将 isBatchingUpdates 设置为 false,执行 batchingStrategy.batchedUpdates 合并全部状态,再更新。

react 中有一个更新策略对象 ReactDefaultBatchingStrategy,主要用于维护批量更新状态(isBatchingUpdates)和启动更新事务(batchedUpdates

// 更新事务的 wrappers
var TRANSACTION_WRAPPERS = [FLUSH_BATCHED_UPDATES, RESET_BATCHED_UPDATES];
// 修改批量更新状态
var RESET_BATCHED_UPDATES = {
initialize: emptyFunction,
close: function () {
ReactDefaultBatchingStrategy.isBatchingUpdates = false;
}
};
// 遍历所有的 dirtyComponents 更新组件
var FLUSH_BATCHED_UPDATES = {
initialize: emptyFunction,
close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates)
};
// ...
// 更新策略对象
var ReactDefaultBatchingStrategy = {
// 是否批量更新
isBatchingUpdates: false,

batchedUpdates: function (callback, a, b, c, d, e) {
var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates;
ReactDefaultBatchingStrategy.isBatchingUpdates = true;
if (alreadyBatchingUpdates) {
return callback(a, b, c, d, e);
} else {
return transaction.perform(callback, null, a, b, c, d, e);
}
}
};
// ...
// 更新事务对象,是 Transition 对象的子类
function ReactDefaultBatchingStrategyTransaction() {
// 初始化事务
this.reinitializeTransaction();
}
// ...
// 实现了 getTransactionWrappers 这个抽象方法,即注入更新事务的 wrappers
Object.assign(ReactDefaultBatchingStrategyTransaction.prototype, Transaction, {
getTransactionWrappers: function() {
return TRANSACTION_WRAPPERS;
},
});
...
var transaction = new ReactDefaultBatchingStrategyTransaction();

从源码可以看到,setState 会调用 ReactDefaultBatchingStrategy.batchedUpdates,从而执行 transition.perform 开启更新事务

react 16

react 16 引入了 Fiber 架构,抛弃了 react 15 的事务机制,借助锁和更新链表实现批量更新

关键词:

  • updateQueue(状态更新队列)
  • isBatchingUpdates(锁)

react 16 的组件渲染过程主要分为 render 阶段和 commit 阶段,从执行 setState 开始看下两个阶段都做了什么

主要执行流程如下:

以下源码参考自 v16.8.3

render

这个阶段主要是收集各个 fiber 节点上的更新,将它们追加到 updateQueue 上

// react/packages/react/src/ReactBaseClasses.js
Component.prototype.setState = function (partialState, callback) {
// ...
this.updater.enqueueSetState(this, partialState, callback, "setState");
};

// react/packages/react-reconciler/src/ReactFiberClassComponent.js
const classComponentUpdater = {
// ...
enqueueSetState(inst, payload, callback) {
const fiber = getInstance(inst);
const currentTime = requestCurrentTime();
const expirationTime = computeExpirationForFiber(currentTime, fiber);

// 创建一个更新队列
const update = createUpdate(expirationTime);
update.payload = payload;
if (callback !== undefined && callback !== null) {
// ...
update.callback = callback;
}
// TODO 暂不清楚这个的具体作用
flushPassiveEffects();
// 将更新任务挂载到 fiber 上
enqueueUpdate(fiber, update);
// 调度更新
scheduleWork(fiber, expirationTime);
},
// ...
};

// react/packages/react-reconciler/src/ReactUpdateQueue.js
export function enqueueUpdate<State>(fiber: Fiber, update: Update<State>) {
// Update queues are created lazily.
const alternate = fiber.alternate;
let queue1;
let queue2;
// ...
{
appendUpdateToQueue(queue1, update);
}
// ...
}
// ...
function appendUpdateToQueue<State>(
queue: UpdateQueue<State>,
update: Update<State>
) {
// 将 update 对象挂载到更新队列 updateQueue 上,形成一个环状单向链表
if (queue.lastUpdate === null) {
queue.firstUpdate = queue.lastUpdate = update;
} else {
queue.lastUpdate.next = update;
queue.lastUpdate = update;
}
}

render 阶段,在更新单个 fiber 时(beginWork),会遍历更新队列计算得到最新的 state,该操作由 processUpdateQueue 完成。状态更新过程的调用栈有:beginWork -> updateClassComponent -> updateClassInstance -> processUpdateQueue

// react/packages/react-reconciler/src/ReactUpdateQueue.js
function updateClassInstance(
current: Fiber,
workInProgress: Fiber,
ctor: any,
newProps: any,
renderExpirationTime: ExpirationTime,
): boolean {
// ...
const oldState = workInProgress.memoizedState;
let newState = (instance.state = oldState);
let updateQueue = workInProgress.updateQueue;
if (updateQueue !== null) {
// 遍历更新队列计算得到新值
processUpdateQueue(
workInProgress,
updateQueue,
newProps,
instance,
renderExpirationTime,
);
newState = workInProgress.memoizedState;
}
// ...
}
// ...
// 处理更新队列,返回新的 state
export function processUpdateQueue<State>(
workInProgress: Fiber,
queue: UpdateQueue<State>,
props: any,
instance: any,
renderExpirationTime: ExpirationTime,
): void {
// ...
while (update !== null) {
// ...
{
resultState = getStateFromUpdate(
workInProgress,
queue,
update,
resultState,
props,
instance,
);
// 收集 callback,在 commit 阶段执行
const callback = update.callback;
if (callback !== null) {
workInProgress.effectTag |= Callback;
update.nextEffect = null;
if (queue.lastEffect === null) {
queue.firstEffect = queue.lastEffect = update;
} else {
queue.lastEffect.nextEffect = update;
queue.lastEffect = update;
}
}
}
// Continue to the next update.
update = update.next;
}
// ...
workInProgress.memoizedState = resultState;
// ...
}
// ...
function getStateFromUpdate (..., partialState, ...) {
// ...
if (typeof _payload2 === 'function') {
// ...
// Merge the partial state and the previous state.
return _assign({}, prevState, partialState);
}
}

commit

该阶段又分为三个子阶段

  • before mutation
  • mutation
  • layout

在第一个阶段,classComponent 会更新当前的 state 和 props(实际计算新值的操作在render阶段的beginWork中执行,并赋值给 fiber 节点的memorizedStatememorizedProps

其中执行 setStatecallback 发生在第三个阶段 layout,入口代码如下:

// react/packages/react-reconciler/src/ReactFiberScheduler.js
// ...
while (nextEffect !== null) {
try {
commitAllLifeCycles(root, committedExpirationTime);
} catch (e) {
// ...
}
}
// ...

commitAllLifeCycles 这个函数内主要调用了 commitLifeCycles

// react/packages/react-reconciler/src/ReactFiberCommitWork.js
function commitLifeCycles(
finishedRoot: FiberRoot,
current: Fiber | null,
finishedWork: Fiber,
committedExpirationTime: ExpirationTime
): void {
switch (finishedWork.tag) {
// ...
case ClassComponent: {
const instance = finishedWork.stateNode;
// ...
const updateQueue = finishedWork.updateQueue;
if (updateQueue !== null) {
// ...
commitUpdateQueue(
finishedWork,
updateQueue,
instance,
committedExpirationTime
);
}
return;
}
// ...
}
}

commitUpdateQueue 会遍历更新队列执行 setStatecallback

// react/packages/react-reconciler/src/ReactUpdateQueue.js
export function commitUpdateQueue<State>(
finishedWork: Fiber,
finishedQueue: UpdateQueue<State>,
instance: any,
renderExpirationTime: ExpirationTime
): void {
// If the finished render included captured updates, and there are still
// lower priority updates left over, we need to keep the captured updates
// in the queue so that they are rebased and not dropped once we process the
// queue again at the lower priority.
if (finishedQueue.firstCapturedUpdate !== null) {
// Join the captured update list to the end of the normal list.
if (finishedQueue.lastUpdate !== null) {
finishedQueue.lastUpdate.next = finishedQueue.firstCapturedUpdate;
finishedQueue.lastUpdate = finishedQueue.lastCapturedUpdate;
}
// Clear the list of captured updates.
finishedQueue.firstCapturedUpdate = finishedQueue.lastCapturedUpdate = null;
}

// Commit the effects
commitUpdateEffects(finishedQueue.firstEffect, instance);
finishedQueue.firstEffect = finishedQueue.lastEffect = null;

commitUpdateEffects(finishedQueue.firstCapturedEffect, instance);
finishedQueue.firstCapturedEffect = finishedQueue.lastCapturedEffect = null;
}

function commitUpdateEffects<State>(
effect: Update<State> | null,
instance: any
): void {
while (effect !== null) {
const callback = effect.callback;
if (callback !== null) {
effect.callback = null;
callCallback(callback, instance);
}
effect = effect.nextEffect;
}
}

批量更新

在 react 15 是借助了事务和锁实现了批量更新,在 react 16 是怎么实现的呢?

比如我们触发了一个合成事件 onClick,会触发 dispatchEvent ,执行到 batchedUpdates 函数

function batchedUpdates<A, R>(fn: (a: A) => R, a: A): R {
const previousIsBatchingUpdates = isBatchingUpdates; // 默认是 false
isBatchingUpdates = true;
try {
return fn(a);
} finally {
isBatchingUpdates = previousIsBatchingUpdates;
if (!isBatchingUpdates && !isRendering) {
performSyncWork();
}
}
}

这里同样是用了锁(isBatchingUpdates)来开启批量更新模式,所以如果我们在合成事件(fn)里面触发 多次setState,react 会根据 isBatchingEventUpdates = true 判断当前处于批量更新模式,会把更新任务挂载到更新队列上,当合成事件执行完毕后才统一调度更新

其他

react 17 和 react 16 其他版本,其实跟 react v16.8.3 实现 setState 的思路大同小异(后面有空再补上最新版本的)。大体上都是产生一个 update 对象去承载新的状态。多个 update 对象会连接成一个环状单向链表 - updateQueue 并挂载 fiber 上, 然后在更新该 fiber 的时候 (beginWork)会循环该 updateQueue,依次处理其中的 update,计算得到最新的 state

· 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 拆分成两个阶段:rendercommit。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(如果有副作用)。completeUnitOfWorkcompleteWork
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 操作完成后,读取组件的状态,对于类组件,会调用生命周期componentDidMountcomponentDidUpdate;对于函数式组件,主要执行 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
}
}
}