大纲
react 源码解析,聚焦于下面的几个话题:
1⃣️ 为什么在 react 中,不应该定义闭包函数式组件。
2⃣️ bailout、eager bailout 是什么?
3⃣️ react 中,再也不需要写 memo。react compiler (原名 react forget) 的使用。
注:每个代码片段都有标记出处:GitHub url 和 代码行号。方便读者跳转到完整的源码位置。
观前提示 (看不懂可以直接跳到前言)
本文源自于我给 react 发的 PR,官方 CI 大测试,只有 prettier 没过。
单元测试通过,证明了理论上的可行性。
https://github.com/facebook/react/pull/34116
React 并不优秀,只是使用的人多。
前端,redux 因为诞生得早,使用广泛,实际上并不好用。
Pinia 借助底层的响应式数据,实现了相同的功能,且语法更漂亮和简洁。
React 底层架构设计的缺陷,导致了其很多复杂性。
如 react 的异步特性 (startTransition concurrent scheduler),是因为其内部缺少响应式系统、静态模板编译优化而诞生的。
react 数据(状态)改变时,react 无法直接找出被改变的组件,必须得 re-render 全部组件,这样的 re-render 导致了性能问题。
react 为了向后兼容以及降低复杂性,只能用 memo 和 异步渲染 来解决这个问题。
我们可以大胆幻想下,如果直接在 useState 上添加响应式数据,因为 useState 返回的是值,而不是对象,这会导致无法用 proxy 设置 getter,无法记录依赖。
如果新创建一个 useProxy hook,是可行的。只不过 react 核心团队的理念是 “少做少错,多做多错”,一周上三天,就连 react 内部的 scheduler,从2019 年开发到现在 2025,接口都还不稳定。他们效率太低。
他们的状态估计是跟当年占据 90% 市场份额的 IE 差不多,我实在无法想明白 IE 为什么不继续维护了。我能想到模糊的解释就是: 不思进取、开发重心转变、大企业的办公室政治。
希望墙倒众人推,希望 react 走向时代的尘埃。混杂着个人恩怨,我是不打算继续在个人项目中使用 react 了。
前言
我本来想尝试 将文章写成更通俗易懂,由浅入深地讲解整个 react 代码架构,但我发现这是一个天真的想法。
想要讲清整个 react 的机理,至少需要一本书的篇幅,以及大量图文。市面上暂时没有中文书,可以做到。
如果把巨量的信息,压缩到一篇文章内,势必会导致巨量的信息丢失,文字碎片化,阅读体验非常跳跃,晦涩难懂,估计只会变成只有作者才能读懂的忍者符号。
所以本文更多的不是教学向的入门教程,而是源码简化、索引、整理。源码的 GitHub URL 地址,我都会标记清楚,方便你直接跳转,查看完整源码。
如果你想学习框架源码,通过 Vue 上手框架源码将会是更好的选择。
Vue 模块划分清晰,而且还有霍春阳老师编写的《Vuejs设计与实现》。
霍春阳老师写作水平超一流,叙事由浅入深,由简单的小概念验证,一步步完善边界情况,将代码变成一个小框架。
Vue 和 React 有很多概念有重合,如 虚拟 DOM (通过 JS 对象表示 DOM 节点),和基于虚拟 DOM 的 diff 算法 (对比新旧虚拟 DOM,找出变动的地方)。
所以学习了 Vue 源码,React 很多问题也是可以想明白的。
用 Vue 思维解决 React 问题
这里假设你已经阅读完了 《Vuejs设计与实现》
题: React 为什么在函数式组件内部,不应该再定义函数式组件,也就是闭包函数式组件?
import React, { useState } from 'react';
export function App(props) {
return <Child />;
}
function Child(props) {
const [sliderRange, setSliderRange] = useState(0);
return <Slider />;
function Slider() {
return (
<input
value={sliderRange}
onChange={e => setSliderRange(e.target.value)}
type='range'
min='0'
max='20'
/>
);
}
}
这里的 Slider 拖动起来, 并不顺畅, 拖动了一下后, 就拖不动了, 得点击后, 重新拖动, 如此往复.
如果直接问豆包 AI, AI 会回复 "性能问题", 实际上牛头不对马嘴, 这里的不是性能问题.
答: 因为函数式组件 虚拟 DOM 的 tag 属性 (虚拟 DOM 是一个 js 对象,这个对象有个属性 tag) 为函数式组件本身。
而在“单元素 diff 算法”过程中,如果判断到新旧 tag 不一致,diff 算法会认为不应该复用原来的真实 dom。
渲染器 (它会根据虚拟DOM,createElement,并把新建元素添加到页面中,这个过程叫“渲染”)会根据 diff 算法的指示执行渲染任务。
在这个例子中,渲染器会把原来的真实 dom remove 掉,然后重新创建真实 dom。
所以会导致 "拖动不连续" 的问题.
闲谈结束,下面开始完整的源码解析,通过源码解释下面问题:
1) 刚刚提到的闭包组件,diff 算法问题。
2) bailout、eager bailout
3) 再也不用写 memo 了,react compiler (原名 react forget) 的使用
这些都是操作性非常强知识,文章的重点不会聚焦于各种离谱、讳莫如深、望穿秋水都看不透的算法,而是一个问题,对应一个源码知识。
空谈误国,实干兴邦。柴静曾经说过,“事实自有万钧之力”,coderwhy 说过 “只有知道真相,才能获得真正的自由”。
源码就是真相,源码就是事实,源码作为客观存在,是我们理论的依据。我们不会像三流博客一样胡说八道,这里只有真相。
章回一: 组件嵌套图方便,单点更新成梦魇
我们分析的源码版本为: https://github.com/facebook/react/releases/tag/v19.1.1
概念: 虚拟 DOM
虚拟 DOM 就是用 JS 对象表示真实 DOM。也就是给真实 DOM 建模。
使用 JS 对象表示真实 DOM 的好处,就是 JS 对象比真实 DOM 更轻量,成员属性和方法更少。
同时虚拟 DOM 也方便 "跨平台", 在浏览器环境下, 可以渲染为真实 DOM.
在 SSR,也就是 NODE 环境下,可以将虚拟 DOM 渲染为字符串。
React 的虚拟 DOM 就是 ReactElememt 和 fiber。
React 的虚拟 DOM
React 代码经过 babel 编译后的结果,如下面的代码片段所示
在线地址: https://babeljs.io/repl
function App() {
return <Child/>
}
function Child() {
return <div>hello!</div>
}
import { jsx as _jsx } from "react/jsx-runtime";
function App() {
return /*#__PURE__*/_jsx(Child, {}); // 这里我们关注第一个参数, 可以发现是 Child 函数组件本身
}
function Child() {
return /*#__PURE__*/_jsx("div", {
children: "hello!"
});
}
上面代码片段中, _jsx 函数的作用为生成一个 ReactElement 对象.
ReactElement 的类型定义如下所示:
// packages/shared/ReactElementType.js
// https://github.com/facebook/react/blob/v19.1.1/packages/shared/ReactElementType.js
// 第 12 行
export type ReactElement = {
$$typeof: any,
type: any, // 关注这里
key: any,
ref: any,
props: any,
// __DEV__ ...
};
// 顺着 react/jsx-runtime 一路追踪过去, _jsx 最终会调用 jsxProd
// packages/react/src/jsx/ReactJSXElement.js
// https://github.com/facebook/react/blob/v19.1.1/packages/react/src/jsx/ReactJSXElement.js
// 第 306 行
/*
https://github.com/reactjs/rfcs/pull/107
在上面例子中:
function App() {
return _jsx(Child, {});
}
这里的 type 参数的值为 Child
*/
export function jsxProd(type, config, _) {
// ...
// 第 366 行
return ReactElement(
type,
key,
undefined,
undefined,
getOwner(),
props,
undefined,
undefined,
);
}
// 顺着 react/jsx-runtime 一路追踪过去, _jsx 最终会调用 jsxProd
// packages/react/src/jsx/ReactJSXElement.js
// https://github.com/facebook/react/blob/v19.1.1/packages/react/src/jsx/ReactJSXElement.js
/**
* 第 157行
* Factory method to create a new React element
...
*
* @param {*} type
* @param {*} props
...
*/
function ReactElement(
type, /* 在上面的 jsx 中, 这里为 Child */
key,
// ...
props,
// ...
) {
const refProp = props.ref;
//...
// 第 203 行 & 第 241 行
element = {
// This tag allows us to uniquely identify this as a React Element
$$typeof: REACT_ELEMENT_TYPE,
// Built-in properties that belong on the element
type, /* 在上面例子中, App 的 _jsx(Child, {}), 这里的 type 为 Child */
key,
ref,
props,
};
//...
return element;
}
React Element 可以为 函数式组件的实例.
也就是说, 当一个 React Element 对应一个函数式组件. 也就是 一个 React Element 为一个函数式组件的实例.
那么这个 React Element 的 type 属性等于函数式组件.
上面滑块案例经过 babel 编译后的产物:
import React, { useState } from 'react';
import { jsx as _jsx } from "react/jsx-runtime";
export function App(props) {
return /*#__PURE__*/_jsx(Child, {});
}
function Child(props) {
const [sliderRange, setSliderRange] = useState(0);
return /*#__PURE__*/_jsx(Slider, {});
function Slider() {
return /*#__PURE__*/_jsx("input", {
value: sliderRange,
onChange: e => setSliderRange(e.target.value),
type: "range",
min: "0",
max: "20"
});
}
}
我们可以发现, 每次 Child 执行时, 其内部的闭包函数式组件都会重新生成.
也就是 新旧 Silder 的地址值不一致.
也就会导致其闭包函数式组件 (Slider) 对应的 新旧 ReactElement 的 type 会不一样.
也就是每一次 Child 执行时, 生成的 React Element _jsx(Slider, {}).type
不等于 下一次 Child 执行时的 _jsx(Slider, {}).type
这也就会导致前言中描述的问题.
function Foo() {
return inner;
function inner() {
}
}
const inner01 = Foo();
const inner02 = Foo();
console.log(inner01 === inner02) // false
function Foo() {
return {};
}
const inner01 = Foo();
const inner02 = Foo();
console.log(inner01 === inner02) // false
function Foo() {
return new Object();
}
const inner01 = Foo();
const inner02 = Foo();
console.log(inner01 === inner02) // false
在大多数语言中, new 每次都会申请新的内存空间, 类似 C 的 malloc
.
我们继续分析 React 源码
React Element 转 fiber node:
// packages/react-reconciler/src/ReactInternalTypes.js
// https://github.com/facebook/react/blob/v19.1.1/packages/react-reconciler/src/ReactInternalTypes.js
// 第 88 行
// 虚拟 DOM 节点, 类似 React.Element
export type Fiber = {
// ...
// 虚拟 DOM 节点的唯一标识
key: null | string,
// The value of element.type which is used to preserve the identity during
// reconciliation of this child.
// 这里其实就是 React element 的 type, 见下方的 createFiberFromTypeAndProps
elementType: any,
// ...
}
// packages/react-reconciler/src/ReactFiber.js
// https://github.com/facebook/react/blob/v19.1.1/packages/react-reconciler/src/ReactFiber.js
// 第 737 行
export function createFiberFromElement(
element: ReactElement,
mode: TypeOfMode,
lanes: Lanes,
): Fiber {
// ...
const type = element.type;
const key = element.key;
const pendingProps = element.props;
// 第 749 行
const fiber = createFiberFromTypeAndProps(
type,
key,
pendingProps,
_,
mode,
lanes,
);
// ...
return fiber;
}
// 第 546 行
// 这个函数的作用是把 React Element 转化为 Fiber 节点
export function createFiberFromTypeAndProps(
type: any, // React$ElementType
key: null | string,
// ...
): Fiber {
// ...
// 第 725 行
const fiber = createFiber(_, _, key, _);
fiber.elementType = type; // 重点, 这里把 fiber 对应的 React Element 的type 赋值给了 fiber 的 elementType
// ...
return fiber;
}
// packages/react-reconciler/src/ReactChildFiber.js
// https://github.com/facebook/react/blob/v19.1.1/packages/react-reconciler/src/ReactChildFiber.js
// 第 1622 行
function reconcileSingleElement(
// 父 虚拟 DOM 节点
returnFiber: Fiber,
// 旧的 虚拟 DOM 节点, 这里是 FiberNode, 跟 React Element 差不多
currentFirstChild: Fiber | null,
// 新建的 react element 节点
element: ReactElement,
_,
): Fiber {
const key = element.key;
let child = currentFirstChild;
while (child !== null) {
// 单节点 diff 的第一个条件, 新旧虚拟 DOM 的 key 必须相同
if (child.key === key /* element.key 新 react element 节点的 key */) {
const elementType = element.type;
if (elementType === REACT_FRAGMENT_TYPE) {
// 当新建元素为 fragment 的逻辑, 忽略
} else {
if (
// 单节点 diff 的第二个条件, 新旧虚拟 DOM 的 elementType 必须相同
// elementType 可以为 'p' 'div' 这些标签字符串
// 可以为函数式组件
child.elementType === elementType /* ReactElement.type */
// 忽略
) {
// ...
// 复用元素
const existing = useFiber(child, element.props);
// ...
// 这里复用后便 return 了, 结束函数逻辑
return existing;
}
}
// ...
break;
} else {
// 如果上面的复用逻辑都不满足, 则删除旧 DOM
deleteChild(returnFiber, child);
}
child = child.sibling;
}
// 如果上面的复用逻辑都不满足, 则到达这里
if (element.type === REACT_FRAGMENT_TYPE) {
// ...fragment
} else {
// 相当于创建新的节点
const created = createFiberFromElement(element, returnFiber.mode, lanes);
// ...
return created;
}
}
}
通过对这个 reconcileSingleElement 函数 执行逻辑的分析, 我们便找出了本章回问题的答案.
如果 新旧虚拟 DOM 的 key
或elementType
不同, 则不会复用真实 dom, 这里会直接把真实 DOM 卸载掉
这种逻辑在大多数场景下, 都是合理的.
如 <p></p>
变成 <p class="new-classname"></p>
React 渲染器只用使用 setAttribute
修改 props就好 .
但如果是 <p></p>
变成`
, 这时候就需要把旧的真实 DOM 卸载掉, 然后创建一个
div`作为新的真实 DOM, 并挂载上去.
fiber node 的 elementType
可能为标签名, 也可能为函数式组件.
第二章: bailout、eager bailout
问题: 点击 Child1, 控制台的输出是怎么样的?
import React, { useState } from 'react';
export function App(props) {
console.log('App');
return <Child0 />;
}
function Child0(props) {
console.log('child0');
return (
<div>
child0
<Child1 />
</div>
);
}
function Child1(props) {
let [state, setState] = useState(0);
console.log('child1');
return (
<div onClick={() => setState(n => n + 1)}>
child1
<Child2 />
</div>
);
}
function Child2(props) {
console.log('child2');
return (
<div>
child2
<Child3 />
</div>
);
}
function Child3(props) {
console.log('child3');
return <div> child3 </div>;
}
答案:
// 首屏渲染
App
child0
child1
child2
child3
// 点击 Child1
child1
child2
child3
太诡异了, React 不应该每次都是从头到尾, 重新渲染吗?
为什么点击 Child1, App 和 Child0 没有重新渲染.
而且我们还没有使用 memo.
这一现象太超越了, 我第一次看到, 非常震惊, 于是就去搜索和翻源码.
我发现这种现象叫 bailout (跳过), 跳过渲染, 跳过 render.
✅ React bailout 是什么?
bailout(跳过更新)发生在 beginWork 阶段,用于跳过当前 Fiber 的处理,从而提高性能。
bailout 成立的条件主要是:
-
当前组件的 props 和 state 没有变化(包括 context、hooks 的值等)。
-
没有新的更新任务(update queue 为空)。
-
shouldComponentUpdate 返回 false(对于 class 组件)。
-
或 React.memo 的比较函数返回 true(对于函数组件)。
bailout 的结果:
当前 Fiber 节点(wip)会直接 复用 current 的子树(即 current.child),不会继续 beginWork 子节点。
避免不必要的 render & diff。
函数式组件是如何被调用的
对于函数式组件, 我们可以认为其 Fiber Node 的 type 为函数式组件本身.
// packages/react-reconciler/src/ReactFiber.js
// https://github.com/facebook/react/blob/v19.1.1/packages/react-reconciler/src/ReactFiber.js
// 第 546 行
export function createFiberFromTypeAndProps(
type: any, // React$ElementType
key: null | string,
pendingProps: any,
owner: null | ReactComponentInfo | Fiber,
mode: TypeOfMode,
lanes: Lanes,
): Fiber {
// ...
// 第 556 行
let resolvedType = type;
// ...
// 第 725 行
const fiber = createFiber(fiberTag, pendingProps, key, mode);
// 第 727 行
fiber.type = resolvedType;
return fiber;
}
我们的函数式组件,最后会被 RenderWithHooks 调用。
预备知识
一些概念:
work in process fiber node 为当前正在生成和处理的 fiber node。
每个 wip fiber node 都有一个 current / alternate fiber node,也就是屏幕上正在显示的 fiber node。
也就是有两颗虚拟 DOM 树,一颗是 wip 树,一颗是 current 树。
需要两颗虚拟 DOM树的原因是,在协调(reconcile)阶段,也就是 diff 算法阶段,需要对比二者的差异,并将这些差异记录下来.
最后, React 会把差异 作用和渲染到 真实UI界面上。
函数式组件被调用的过程:
// packages/react-reconciler/src/ReactFiberBeginWork.js
// https://github.com/facebook/react/blob/v19.1.1/packages/react-reconciler/src/ReactFiberBeginWork.js
// 第 3809 行
// beginWork 的职责是生成 新的虚拟 DOM 节点
// 而生成虚拟 DOM 节点的方式, 就是通过调用函数组件, 获取其生成的 React Elements
// 然后将这些新的 React Elements 转成 fiber 就完成了工作
function beginWork(
// 旧的 虚拟 DOM 节点
current: Fiber | null,
// 新的 虚拟 DOM 节点还未生成, 这是新的虚拟 DOM 节点的 父节点
workInProgress: Fiber,
_,
): Fiber | null {
// ,,,
switch (workInProgress.tag) {
// 第 3913 行
case FunctionComponent: {
const Component = workInProgress.type;
return updateFunctionComponent(
current,
workInProgress,
Component,
resolvedProps,
renderLanes,
);
}
// ...
}
// packages/react-reconciler/src/ReactFiberBeginWork.js
// https://github.com/facebook/react/blob/v19.1.1/packages/react-reconciler/src/ReactFiberBeginWork.js
// 第 1109 行
function updateFunctionComponent(
// 旧的 虚拟 DOM 节点
current: Fiber | null,
// 新的 虚拟 DOM 节点还未生成, 这是新的虚拟 DOM 节点的 父节点
workInProgress: Fiber,
Component: any,
nextProps: any,
renderLanes: Lanes,
) {
let nextChildren;
// 1181 行 & 1191 行
nextChildren = renderWithHooks(
current,
workInProgress,
Component,
nextProps,
context,
renderLanes,
);
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
return workInProgress.child;
}
// packages/react-reconciler/src/ReactFiberHooks.js
// https://github.com/facebook/react/blob/v19.1.1/packages/react-reconciler/src/ReactFiberHooks.js
// 第 551 行
export function renderWithHooks<Props, SecondArg>(
// 旧的 虚拟 DOM 节点
current: Fiber | null,
// 新的 虚拟 DOM 节点还未生成, 这是新的虚拟 DOM 节点的 父节点
workInProgress: Fiber,
Component: (p: Props, arg: SecondArg) => any,
props: Props,
secondArg: SecondArg,
nextRenderLanes: Lanes,
): any {
// 第 645 行
let children = __DEV__
? callComponentInDEV(Component, props, secondArg)
: Component(props, secondArg);
return children;
}
综上所述, 函数式组件最后会在 renderWithHooks 中, 被调用
bailout
// packages/react-reconciler/src/ReactFiberBeginWork.js
// https://github.com/facebook/react/blob/v19.1.1/packages/react-reconciler/src/ReactFiberBeginWork.js
// 第 3809 行
// beginWork 的职责是生成 新的虚拟 DOM 节点
// 而生成虚拟 DOM 节点的方式, 就是通过调用函数组件, 获取其生成的 React Elements
// 然后将这些新的 React Elements 转成 fiber 就完成了工作
function beginWork(
// 旧的 虚拟 DOM 节点
current: Fiber | null,
// 新的 虚拟 DOM 节点还未生成, 这是新的虚拟 DOM 节点的 父节点
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
if (current !== null) {
const oldProps = current.memoizedProps; // 旧虚拟 DOM 节点的 props
const newProps = workInProgress.pendingProps; // 新虚拟 DOM 节点的 props
if (
// bailout 第一个条件, props 不变
oldProps !== newProps ||
// bailout 第二个条件, context 不变
hasLegacyContextChanged() /* context 是否变化 */
) {
// didReceiveUpdate 是全局变量, 这里代表没有命中 bailout
didReceiveUpdate = true;
} else {
// 用户输入会触发事件, 事件内部可能通过 setState, 派发了一个 Update
// 如 setState(5), 就会创建一个 Update 对象, Update 内部保存 5
// 这里检查是否有待处理的 Update, 也就是说这里是 bailout 第三个条件, state 是否变化
const hasScheduledUpdateOrContext = checkScheduledUpdateOrContext(
current,
renderLanes,
);
if (
!hasScheduledUpdateOrContext &&
) {
// No pending updates or context. Bail out now.
// 没有更新! Bail out!!!!
didReceiveUpdate = false;
return attemptEarlyBailoutIfNoScheduledUpdate(
current,
workInProgress,
renderLanes,
);
}
}
} else {
// .....
}
workInProgress.lanes = NoLanes;
switch (workInProgress.tag) {
case FunctionComponent: {
const Component = workInProgress.type;
return updateFunctionComponent(
current,
workInProgress,
Component,
_,
_
);
}
}
throw new Error(
`Unknown unit of work tag (${workInProgress.tag}). This error is likely caused by a bug in ` +
'React. Please file an issue.',
);
}
为了解释 checkScheduledUpdateOrContext 的内部逻辑
我们需要翻到 useState 返回的 dispatch 的内部逻辑里面.
什么是 dispatch? 也就是 setState.
const [state, setState] = useState
这里的 setState 就是 dispatch.
hook 背后的真相
我们函数式组件调用的 useState,可能为 mountState 和 updateState:
// packages/react-reconciler/src/ReactFiberHooks.js
// https://github.com/facebook/react/blob/v19.1.1/packages/react-reconciler/src/ReactFiberHooks.js
// 第 551 行
export function renderWithHooks<Props, SecondArg>(
// 旧的 虚拟 DOM 节点
current: Fiber | null,
// 新的 虚拟 DOM 节点还未生成, 这是新的虚拟 DOM 节点的 父节点
workInProgress: Fiber,
Component: (p: Props, arg: SecondArg) => any,
props: Props,
secondArg: SecondArg,
nextRenderLanes: Lanes,
): any {
//...
if (__DEV__) {
// 第 596 行
if (current !== null && current.memoizedState !== null /* THIS!!!*/ ) {
ReactSharedInternals.H = HooksDispatcherOnUpdateInDEV;
} // ....
else {
ReactSharedInternals.H = HooksDispatcherOnMountInDEV;
}
} else {
// 第 609 行
ReactSharedInternals.H =
current === null || current.memoizedState === null /* THIS!!!*/
? HooksDispatcherOnMount
: HooksDispatcherOnUpdate;
}
// ...
let children = __DEV__
? callComponentInDEV(Component, props, secondArg)
: Component(props, secondArg);
}
也就是说, renderWithHooks 会判断 wip 的 fiber 是否有 alternate, 来将 Hook 上下文 (ReactSharedInternals.H) 设置成 HooksDispatcherOnMount
或 HooksDispatcherOnUpdate
.
// packages/react/src/ReactHooks.js
// https://github.com/facebook/react/blob/v19.1.1/packages/react/src/ReactHooks.js
// 第 72 行
export function useState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
const dispatcher = resolveDispatcher();
return dispatcher.useState(initialState);
}
// packages/react/src/ReactHooks.js
// https://github.com/facebook/react/blob/v19.1.1/packages/react/src/ReactHooks.js
// 第 30 行
function resolveDispatcher() {
const dispatcher = ReactSharedInternals.H;
// ...
return ((dispatcher: any): Dispatcher);
}
// packages/react-reconciler/src/ReactFiberHooks.js
// https://github.com/facebook/react/blob/v19.1.1/packages/react-reconciler/src/ReactFiberHooks.js
// 第 4183 行
const HooksDispatcherOnMount: Dispatcher = {
// ...
useMemo: mountMemo,
useReducer: mountReducer,
useRef: mountRef,
useState: mountState,
// ...
};
// 第 4217 行
const HooksDispatcherOnUpdate: Dispatcher = {
// ...
useMemo: updateMemo,
useReducer: updateReducer,
useRef: updateRef,
useState: updateState,
// ...
};
综上所述, useState 对应的实现有两个, 一个是 mountState, 一个是 updateState.
我们看 mountState.
这是 hook obj:
// packages/react-reconciler/src/ReactFiberHooks.js
// https://github.com/facebook/react/blob/v19.1.1/packages/react-reconciler/src/ReactFiberHooks.js
// 第 199 行
export type Hook = {
memoizedState: any,
// ... 并发支持相关代码, 无需分析
queue: any,
next: Hook | null,
};
// packages/react-reconciler/src/ReactFiberHooks.js
// https://github.com/facebook/react/blob/v19.1.1/packages/react-reconciler/src/ReactFiberHooks.js
// 第 1940 行
function mountState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
// 创建 useState 对应的 hook obj
// 我们背诵面试八股文的时候, 都知道, 每个 hook 调用都对应一个 hook obj
// 这些 hook obj 会形成一个单向链表
// fiber node 的 memoizedState 会指向这个单向链表
const hook = mountStateImpl(initialState);
// 用于存储 update 的 queue
// 多次调用 setState, dispatch 会创建 Update 并放入到这个 queue
// 如果是非并发渲染, 会开一个微任务, 批处理, 计算出最终的 state, 避免多次 render
// 这也是为什么 setState 是异步的
// 如果是并发渲染, 会开一个宏任务
const queue = hook.queue;
const dispatch: Dispatch<BasicStateAction<S>> = (dispatchSetState.bind(
null,
// 全局变量, 也就是调用 useState 对应的 fiber
currentlyRenderingFiber,
queue,
): any);
queue.dispatch = dispatch;
return [hook.memoizedState, dispatch];
}
// packages/react-reconciler/src/ReactFiberHooks.js
// https://github.com/facebook/react/blob/v19.1.1/packages/react-reconciler/src/ReactFiberHooks.js
// 第 3735 行
function dispatchSetState<S, A>(
fiber: Fiber, // 绑定了 currentlyRenderingFiber
queue: UpdateQueue<S, A>, // 绑定了 queue
action: A, // setState 参数
): void {
const lane = requestUpdateLane(fiber);
const didScheduleUpdate = dispatchSetStateInternal(
fiber,
queue,
action,
lane,
);
}
// packages/react-reconciler/src/ReactFiberHooks.js
// https://github.com/facebook/react/blob/v19.1.1/packages/react-reconciler/src/ReactFiberHooks.js
// 第 3765 行
// (dispatchSetState.bind(
// null,
// // 全局变量, 也就是调用 useState 对应的 fiber
// currentlyRenderingFiber,
// queue,
// ): any);
function dispatchSetStateInternal<S, A>(
fiber: Fiber, // 绑定了 currentlyRenderingFiber
queue: UpdateQueue<S, A>, // 绑定了 queue
action: A, // setState 参数
lane: Lane,
): boolean {
const update: Update<S, A> = {
lane,
action,
next: (null: any),
};
// ... update 加入 queue 的逻辑
const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);
if (root !== null) {
// re-render
scheduleUpdateOnFiber(root, fiber, lane);
// ...
return true;
}
}
return false;
}
// packages/react-reconciler/src/ReactFiberConcurrentUpdates.js
// https://github.com/facebook/react/blob/v19.1.1/packages/react-reconciler/src/ReactFiberConcurrentUpdates.js
// 第 121 行
export function enqueueConcurrentHookUpdate<S, A>(
fiber: Fiber,
queue: HookQueue<S, A>,
update: HookUpdate<S, A>,
lane: Lane,
): FiberRoot | null {
enqueueUpdate(fiber, queue, update, lane);
return getRootForUpdatedFiber(fiber);
}
// packages/react-reconciler/src/ReactFiberConcurrentUpdates.js
// https://github.com/facebook/react/blob/v19.1.1/packages/react-reconciler/src/ReactFiberConcurrentUpdates.js
// 第 96 行
function enqueueUpdate(
fiber: Fiber,
queue: ConcurrentQueue | null,
update: ConcurrentUpdate | null,
lane: Lane,
) {
// 相当于给 fiber 打个 flag, 标记这个 fiber 上有 state 更新
fiber.lanes = mergeLanes(fiber.lanes, lane);
const alternate = fiber.alternate;
if (alternate !== null) {
alternate.lanes = mergeLanes(alternate.lanes, lane);
}
}
也就是说 dispatch 会给 fiber 设置 lane 标记, 标记这个 fiber 有 state 更新.
也就是说, 如果 state 没更新, 这个 fiber 上, 将会没有标记 lane.
这里, 我们就可以理解 checkScheduledUpdateOrContext 的内部逻辑了.
这里使用 includesSomeLane, 是为了判断是否之前存在未处理的更新.
// packages/react-reconciler/src/ReactFiberBeginWork.js
// https://github.com/facebook/react/blob/v19.1.1/packages/react-reconciler/src/ReactFiberBeginWork.js
// 第 3570 行
// 检查是否有 state 变化
function checkScheduledUpdateOrContext(
// 旧的 fiber 节点
current: Fiber,
renderLanes: Lanes,
): boolean {
const updateLanes = current.lanes;
if (includesSomeLane(updateLanes, renderLanes)) {
return true;
}
return false;
}
bailout 缺陷
我们可以看到 begin work 逻辑中, bailout 的 props 判断是用 ===
比较的.
if (
// bailout 第一个条件, props 不变
oldProps !== newProps ||
// bailout 第二个条件, context 不变
hasLegacyContextChanged() /* context 是否变化 */
) {
这导致了我们示例代码, 尽管只是 Child1 状态变化了, 其子组件也会重新渲染
// 首屏渲染
App
child0
child1
child2
child3
// 点击 Child1
child1
child2
child3
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
function Child1(props) {
let [state, setState] = useState(0);
console.log('child1');
return /*#__PURE__*/_jsxs("div", {
onClick: () => setState(n => n + 1),
children: ["child1", /*#__PURE__*/_jsx(Child2, {})]
});
}
_jsx(Child2, {}
每次函数执行, 传入的 {}
都是新建的
所以导致 bailout miss, Child2 组件重新渲染.
这时候, 我们就需要给 Child2 添加 memo, 让其对新旧 props 进行 ShallowEqual 比较.
eager state
import React, { useState } from 'react';
export function App() {
console.log('App');
return <Child0 />;
}
function Child0() {
let [state, setState] = useState(0);
console.log('child0', state);
return (
<div onClick={() => setState(0)}>
child0
<Child1 />
</div>
);
}
function Child1() {
console.log('child1');
console.log('=======');
return <div>child1</div>;
}
多次点击 Child0, 查看其执行结果
首屏渲染:
App
child0 0
child1
=======
第一次点击:
(无)
第二次点击:
(无)
// packages/shared/objectIs.js
// https://github.com/facebook/react/blob/v19.1.1/packages/shared/objectIs.js
/**
* inlined Object.is polyfill to avoid requiring consumers ship their own
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is
* Object is 的 polyfill
*/
function is(x: any, y: any) {
return (
(x === y && (x !== 0 || 1 / x === 1 / y)) || (x !== x && y !== y) // eslint-disable-line no-self-compare
);
}
const objectIs: (x: any, y: any) => boolean =
// $FlowFixMe[method-unbinding]
typeof Object.is === 'function' ? Object.is : is;
export default objectIs;
// packages/react-reconciler/src/ReactFiberHooks.js
// https://github.com/facebook/react/blob/v19.1.1/packages/react-reconciler/src/ReactFiberHooks.js
// 第 3765 行
// (dispatchSetState.bind(
// null,
// // 全局变量, 也就是调用 useState 对应的 fiber
// currentlyRenderingFiber,
// queue,
// ): any);
function dispatchSetStateInternal<S, A>(
fiber: Fiber, // 绑定了 currentlyRenderingFiber
queue: UpdateQueue<S, A>, // 绑定了 queue
action: A, // setState 参数
lane: Lane,
): boolean {
const update: Update<S, A> = {
lane,
revertLane: NoLane,
action,
next: (null: any),
};
// 注意: 这里是我简化的源码
// 策略逻辑, setState(5), setState(num => num + 1)
function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {
return typeof action === 'function' ? action(state) : action;
}
const alternate = fiber.alternate;
if (
fiber.lanes === NoLanes &&
(alternate === null || alternate.lanes === NoLanes)
) {
// The queue is currently empty, which means we can eagerly compute the
// next state before entering the render phase. If the new state is the
// same as the current state, we may be able to bail out entirely.
// 翻译:
// Update 队列当前为空,这意味着我们可以在进入渲染阶段之前,
// 提前计算下一个状态。如果新状态与当前状态相同,我们或许可以 eager bailout
const currentState: S = (queue.lastRenderedState: any);
const eagerState = basicStateReducer(currentState, action);
// import is from 'shared/objectIs';
if (is(eagerState, currentState)) {
// Fast path. We can bail out without scheduling React to re-render.
// It's still possible that we'll need to rebase this update later,
// if the component re-renders for a different reason and by that
// time the reducer has changed.
// 无需 re-render
return false;
}
}
const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);
if (root !== null) {
// re-render
scheduleUpdateOnFiber(root, fiber, lane);
return true;
}
return false;
}
这里可以看到如果计算出来的前后 state object is 判断为 true, 则不会触发 re-render
第三章 React Compiler
React Compiler 可以自动帮我们添加 memo
useCallback
官方文档: https://react.dev/learn/react-compiler
官方安装教程: https://react.dev/learn/react-compiler/installation
pnpm install -D babel-plugin-react-compiler@rc
在 vite 中使用:
// vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [
react({
babel: {
plugins: ['babel-plugin-react-compiler'],
},
}),
],
});
打开 react 开发者工具的 components 面板, 如果显示:
App Memo✨
└─ Child0 Memo✨
└─ Child1 Memo✨
....
就代表成功了
react-compiler, 等了 4 年, 终于进入最后测试阶段了,
使用它, 便可以再也不用写 const CpnName = memo(() => {})
, useCallback
.
react 代码变得非常漂亮, 直接 function CpnName()
.
打开 react 开发者工具, 也不会满屏的匿名组件了
杂谈
react 的 “虚拟 DOM ”有两个: JSX (React.createElement) 以及 fiber。
“虚拟 DOM”就是用 js object 来表示 DOM。
所以 jsx 和 fiber 都是 js 对象。
“虚拟 DOM”的设计有一个好处——跨平台,浏览器环境渲染为真实 DOM,NodeJS SSR 环境渲染为字符串。
但 fiber 是什么?
react 16、react 17、react 18 憋了三个版本的大招,就是完整的 fiber 架构。
fiber 中文意思为 “纤维”,其实是 “纤程”,类似于 async await 或 goland 的协程。
注意这里 fiber 架构和 fiber node 不是同一个概念,fiber node 是一个虚拟 DOM 节点,一个对象,这里谈到的纤程是纤程,不是 fiber node。
“纤程”类似“线程”,线程是由操作系统调度,可中断的执行任务。
“调度”代表“线程”有优先级的概念,高优先级的线程可以抢占到更多的 cpu 时间片,也就是可以运行得更久。
“纤程”也有优先级,由 react 的 scheduler 调度器调度。高优先级的纤程可以打断低优先级。
“纤程”是用来执行任务的,纤程执行的任务就是 re-render,自顶向下(DFS) 逐个调用函数式组件。
如果页面非常复杂,re-render 的过程可能非常慢。js 是单线程的,如果主线程卡死,UI 就会卡死,如 hover 按钮,按钮样式无反应。
我们 startTransition 内部 setState 就可以开启一个低优先级的渲染任务。
低优先级任务是可以被打断的,也就是在 startTransition 开启的渲染任务内,可以 re-render 一会,再中断去处理用户输入 (高优先级任务),再 re-render。
最高优先级的纤程是同步执行的,也就是 Legacy 模式,默认模式,我们直接 setState 触发的就是同步执行。
startTransition 根据官方示例,是用于 tab 切换时,渲染大量耗时组件,仍然可以空出时间片,去响应用户输入,如 hover 在按钮上,按钮样式变化。
地址: https://react.docschina.org/reference/react/useTransition
也就是说 startTransition 大概率是用在路由切换上,作为底层基础设施,我们很少使用 startTransition,所以这里不展开分析。
同时 Vue3 放弃了 time slice 也就是并发渲染,尤雨溪团队认为复杂且不必要。too complex,little gain。
GitHub issue 地址: https://github.com/vuejs/rfcs/issues/89
仁者见仁,智者见智。毕竟 react 默认正常 setState 触发的都是不可中断的渲染。
这可能说明,react 在极端场景,可能比 Vue 更有优势,用户体验更顺畅。
并发模式
不过,并发模式,算是 react 最大的特色了。
所以有人戏称 “vue 比 react 更 react,所以 vue 才是 react,react 应该叫 schedule”。
“vue 比 react 更 react”是指,每次状态变动,vue 利用响应式数据便可以直接重新渲染对应的组件,而 react 却要自顶向下(DFS)重新渲染所有组件,才能找到变化的位置。
不过 react,组件全部用 memo 性能优化后,可以避免掉大量不必要的渲染。
react 的并发和任务调度,就连 Preact 都没实现,react 18 后, react 和 类 react 框架 的差异会越来越大, 第三方库越来越不兼容。
其他碎片
react 为什么不直接使用 async await? 因为不够灵活,JS 本身的异步,没有优先级调度的概念。
kotlin 的协程是可取消的, 且有优先级调度.
还有 JS 本身有一些缺陷, 如: const 不能 lateinit。
let isFoo = false;
const num;
if (isFoo) {
num = 10;
} else {
num = 20;
}
console.log(num);
// SyntaxError: Missing initializer in const declaration
// 在 kotlin 种, var 是定义 const, val 是定义变量
val isFoo = false
// 类型后置, 如同 TS 一样, 符合人的阅读习惯, 这是一个变量, 这个变量的类型是 XXX
lateinit var num: Int
if (isFoo) {
num = 10
} else {
num = 20
}
println(num)
尾声
文章到这里就结束了, 感谢你的阅读!