上一主题 下一主题
ScriptCat,新一代的脚本管理器脚本站,与全世界分享你的用户脚本油猴脚本开发指南教程目录
返回列表 发新帖

React 源码解析

[复制链接]
  • TA的每日心情
    奋斗
    2025-4-23 20:09
  • 签到天数: 7 天

    [LV.3]偶尔看看II

    20

    主题

    18

    回帖

    391

    积分

    荣誉开发者

    积分
    391

    油中2周年新人报道荣誉开发者

    发表于 3 天前 | 显示全部楼层 | 阅读模式

    大纲

    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 的 keyelementType 不同, 则不会复用真实 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 成立的条件主要是:

    1. 当前组件的 props 和 state 没有变化(包括 context、hooks 的值等)。

    2. 没有新的更新任务(update queue 为空)。

    3. shouldComponentUpdate 返回 false(对于 class 组件)。

    4. 或 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) 设置成 HooksDispatcherOnMountHooksDispatcherOnUpdate.

    // 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)

    尾声

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

    已有1人评分好评 油猫币 理由
    王一之 + 1 + 4 ggnb!

    查看全部评分 总评分:好评 +1  油猫币 +4 

  • TA的每日心情
    郁闷
    2025-7-22 00:22
  • 签到天数: 221 天

    [LV.7]常住居民III

    311

    主题

    4933

    回帖

    4593

    积分

    管理员

    积分
    4593

    管理员荣誉开发者油中2周年生态建设者喜迎中秋油中3周年挑战者 lv2

    发表于 前天 10:10 | 显示全部楼层
    太硬核了,ggnb
    上不慕古,下不肖俗。为疏为懒,不敢为狂。为拙为愚,不敢为恶。
    回复

    使用道具 举报

  • TA的每日心情
    奋斗
    2025-4-23 20:09
  • 签到天数: 7 天

    [LV.3]偶尔看看II

    20

    主题

    18

    回帖

    391

    积分

    荣誉开发者

    积分
    391

    油中2周年新人报道荣誉开发者

    发表于 前天 11:31 | 显示全部楼层

    感谢哥哥的捧场!
    回复

    使用道具 举报

    发表回复

    本版积分规则

    快速回复 返回顶部 返回列表