React 源码解析

大纲

react 源码解析,聚焦于下面的几个话题:

1⃣️ 为什么在 react 中,不应该定义闭包函数式组件。

2⃣️ bailout、eager bailout 是什么?

3⃣️ react 中,再也不需要写 memo。react compiler (原名 react forget) 的使用。

注:每个代码片段都有标记出处:GitHub url 和 代码行号。方便读者跳转到完整的源码位置。

观前提示 (看不懂可以直接跳到前言)

本文源自于我给 react 发的 PR,官方 CI 大测试,只有 prettier 没过。

单元测试通过,证明了理论上的可行性。

github.com/facebook/re...

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 为什么在函数式组件内部,不应该再定义函数式组件,也就是闭包函数式组件?

ini 复制代码
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 说过 "只有知道真相,才能获得真正的自由"。

源码就是真相,源码就是事实,源码作为客观存在,是我们理论的依据。我们不会像三流博客一样胡说八道,这里只有真相。

章回一: 组件嵌套图方便,单点更新成梦魇

我们分析的源码版本为: github.com/facebook/re...

概念: 虚拟 DOM

虚拟 DOM 就是用 JS 对象表示真实 DOM。也就是给真实 DOM 建模。

使用 JS 对象表示真实 DOM 的好处,就是 JS 对象比真实 DOM 更轻量,成员属性和方法更少。

同时虚拟 DOM 也方便 "跨平台", 在浏览器环境下, 可以渲染为真实 DOM.

在 SSR,也就是 NODE 环境下,可以将虚拟 DOM 渲染为字符串。

React 的虚拟 DOM 就是 ReactElememt 和 fiber。

React 的虚拟 DOM

React 代码经过 babel 编译后的结果,如下面的代码片段所示

在线地址: babeljs.io/repl

javascript 复制代码
function App() {
    return <Child/>
}
​
function Child() {
    return <div>hello!</div>
}
javascript 复制代码
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 的类型定义如下所示:

typescript 复制代码
// 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__ ...
};
typescript 复制代码
// 顺着 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,
  );
}
​
​
rust 复制代码
// 顺着 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 编译后的产物:

php 复制代码
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

这也就会导致前言中描述的问题.

ini 复制代码
function Foo() {
    return inner;
​
    function inner() {
        
    }
}
​
const inner01 = Foo();
const inner02 = Foo();
​
console.log(inner01 === inner02) // false
ini 复制代码
function Foo() {
    return {};
}

const inner01 = Foo();
const inner02 = Foo();

console.log(inner01 === inner02) // false
ini 复制代码
function Foo() {
    return new Object();
}

const inner01 = Foo();
const inner02 = Foo();

console.log(inner01 === inner02) // false

在大多数语言中, new 每次都会申请新的内存空间, 类似 C 的 malloc .


我们继续分析 React 源码

React Element 转 fiber node:

typescript 复制代码
// 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,
   
  // ...
}
typescript 复制代码
// 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;
}
javascript 复制代码
// 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 的 keyelementType 不同, 则不会复用真实 dom, 这里会直接把真实 DOM 卸载掉

这种逻辑在大多数场景下, 都是合理的.

<p></p> 变成 <p class="new-classname"></p>

React 渲染器只用使用 setAttribute 修改 props就好 .

但如果是 <p></p>变成<div></div>, 这时候就需要把旧的真实 DOM 卸载掉, 然后创建一个div作为新的真实 DOM, 并挂载上去.

fiber node 的 elementType 可能为标签名, 也可能为函数式组件.

第二章: bailout、eager bailout

问题: 点击 Child1, 控制台的输出是怎么样的?

javascript 复制代码
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>;
}

答案:

arduino 复制代码
// 首屏渲染
App
child0
child1
child2
child3

// 点击 Child1
child1
child2
child3

太诡异了, React 不应该每次都是从头到尾, 重新渲染吗?

为什么点击 Child1, App 和 Child0 没有重新渲染.

而且我们还没有使用 memo.

这一现象太超越了, 我第一次看到, 非常震惊, 于是就去搜索和翻源码.

我发现这种现象叫 bailout (跳过), 跳过渲染, 跳过 render.

✅ React bailout 是什么?

bailout(跳过更新)发生在 beginWork 阶段,用于跳过当前 Fiber 的处理,从而提高性能。

bailout 成立的条件主要是:

  1. 当前组件的 props 和 state 没有变化(包括 context、hooks 的值等)。
  1. 没有新的更新任务(update queue 为空)。
  1. shouldComponentUpdate 返回 false(对于 class 组件)。
  1. 或 React.memo 的比较函数返回 true(对于函数组件)。

bailout 的结果:

当前 Fiber 节点(wip)会直接 复用 current 的子树(即 current.child),不会继续 beginWork 子节点。

避免不必要的 render & diff。

函数式组件是如何被调用的

对于函数式组件, 我们可以认为其 Fiber Node 的 type 为函数式组件本身.

typescript 复制代码
// 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界面上。

函数式组件被调用的过程:

javascript 复制代码
// 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,
      );
    }
    // ...
  }
typescript 复制代码
// 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;
}
typescript 复制代码
// 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

javascript 复制代码
// 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:

typescript 复制代码
// 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) 设置成 HooksDispatcherOnMountHooksDispatcherOnUpdate.

php 复制代码
// 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);
}
ruby 复制代码
// 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);
}
ruby 复制代码
// 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:

typescript 复制代码
// 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,
};
arduino 复制代码
// 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];
}
arduino 复制代码
// 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,
  );
}
arduino 复制代码
// 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;
}
less 复制代码
// 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);
}
php 复制代码
// 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, 是为了判断是否之前存在未处理的更新.

arduino 复制代码
// 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 判断是用 === 比较的.

less 复制代码
    if (
      // bailout 第一个条件, props 不变
      oldProps !== newProps ||
      // bailout 第二个条件, context 不变
      hasLegacyContextChanged() /* context 是否变化 */
    ) {

这导致了我们示例代码, 尽管只是 Child1 状态变化了, 其子组件也会重新渲染

arduino 复制代码
// 首屏渲染
App
child0
child1
child2
child3

// 点击 Child1
child1
child2
child3
javascript 复制代码
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

javascript 复制代码
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, 查看其执行结果

makefile 复制代码
首屏渲染:
App
child0 0
child1
=======

第一次点击:
(无)


第二次点击:
(无)
typescript 复制代码
// 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;
kotlin 复制代码
// 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

官方文档: react.dev/learn/react...

官方安装教程: react.dev/learn/react...

css 复制代码
pnpm install -D babel-plugin-react-compiler@rc

在 vite 中使用:

javascript 复制代码
// 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 面板, 如果显示:

erlang 复制代码
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 在按钮上,按钮样式变化。

地址: react.docschina.org/reference/r...

也就是说 startTransition 大概率是用在路由切换上,作为底层基础设施,我们很少使用 startTransition,所以这里不展开分析。

同时 Vue3 放弃了 time slice 也就是并发渲染,尤雨溪团队认为复杂且不必要。too complex,little gain。

GitHub issue 地址: github.com/vuejs/rfcs/...

仁者见仁,智者见智。毕竟 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。

ini 复制代码
let isFoo = false;
const num;

if (isFoo) {
    num = 10;
} else {
    num = 20;
}

console.log(num);

// SyntaxError: Missing initializer in const declaration
dart 复制代码
// 在 kotlin 种, var 是定义 const, val 是定义变量
val isFoo = false

// 类型后置, 如同 TS 一样, 符合人的阅读习惯, 这是一个变量, 这个变量的类型是 XXX
lateinit var num: Int

if (isFoo) {
    num = 10
} else {
    num = 20
}

println(num)

尾声

文章到这里就结束了, 感谢你的阅读!

相关推荐
崔庆才丨静觅10 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby606111 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了11 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅11 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅11 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅12 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment12 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅12 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊12 小时前
jwt介绍
前端
爱敲代码的小鱼12 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax