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)

尾声

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

相关推荐
光影少年2 小时前
Typescript工具类型
前端·typescript·掘金·金石计划
北风GI2 小时前
如何在 vue3+vite 中使用 Element-plus 实现 自定义主题 多主题切换
前端
月亮慢慢圆2 小时前
网络监控状态
前端
_AaronWong2 小时前
实现 Electron 资源下载与更新:实时进度监控
前端·electron
Doris_20232 小时前
Python条件判断语句 if、elif 、else
前端·后端·python
Doris_20232 小时前
Python 模式匹配match case
前端·后端·python
森林的尽头是阳光2 小时前
vue防抖节流,全局定义,使用
前端·javascript·vue.js
YiHanXii2 小时前
React.memo 小练习题 + 参考答案
前端·javascript·react.js
zero13_小葵司2 小时前
Vue 3 前端工程化规范
前端·javascript·vue.js