React 源码:ReactElement 和 FiberNode 是什么?

2022-12-21 20:07:16 浏览数 (1)

大家好,我是前端西瓜哥。今天学习一下 ReactElement 和 FiberNode。

文中源码来自 React 18.2.0

ReactElement

我们写 React 组件的时候,会使用 JSX,比如:

代码语言:javascript复制
function Component() {
  return (
    <div className="app">
      <span>hello</span>, world
    </div>
  )
}

会编译成多层嵌套的 React.createElement 函数调用。

代码语言:javascript复制
function Component() {
  return React.createElement(
    "div",
    {
      className: "app",
    },
    React.createElement("span", null, "hello"),
    ", world"
  );
}

React.creatElement 源码: https://github.com/facebook/react/blob/1ad8d81292415e26ac070dec03ad84c11fbe207d/packages/react/src/ReactElement.js#L362

React.createElement 在 react 包中,接受:

  1. 元素的 type;
  2. 组件的 props(包括 key 和 ref);
  3. 其余参数则是子元素,同样是 ReactElement 类型;

该方法会返回一个对象,这个对象就是 ReactElement。

ReactElement 的结构为:

代码语言:javascript复制
{
  // 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: type,
  key: key,
  ref: ref,
  props: props,

  // Record the component responsible for creating this element.
  _owner: owner,
};

ReactElement 对象是使用了同名的 ReactElement 函数创建的字面量对象,源码: https://github.com/facebook/react/blob/1ad8d81292415e26ac070dec03ad84c11fbe207d/packages/react/src/ReactElement.js#L148

具体讲解一下:

  • $$typeof:一个标识,值为 Symbol(react.element)。仅仅用于判断当前对象是否为 ReactElement。react 也暴露了一个 isValidElement 方法来做这个判断;
  • type 用于表示类型。可以是原生元素,用字符串表示,比如 "div",或者是用户自己写的函数组件或是类组件,以及 React 内置的特殊组件,会用 symbol 表示,比如 Symbol(react.fragment)Symbol.for('react.strict_mode') 等等。
  • key / ref:就是我们 props 里的 key 和 ref。
  • props:去掉 key 和 ref 后的 props 对象,比如 className、style、children
  • _owner:指向这个 ReactElement 的创建者通过 render 调用所对应的 FiberNode,在上面的例子中,创建者就是函数组件 Component。不通过组件产生的 ReactElement 的 _owner 为 null。

我们可以将 ReactElement 认为是一个虚拟 DOM(为描述更简洁,我称作 vdom),用来做新旧虚拟 DOM 树的对比。

是否需要引入 React

可以看到,编译出的代码中含有 React 变量,所以我们其实是需要手动引入 React,像下面这样:

代码语言:javascript复制
import React from 'react';

上面这种是旧的版本的写法,现在可以不手动导入了,新的脚手架会帮你默认导入。

如果你是自己配置的,需要在相关的 babel 插件下,设置 {"runtime": "automatic"} 。此时会编译为类似下面的代码:

代码语言:javascript复制
// 此行会在编译时自动引入
import {jsx as _jsx} from 'react/jsx-runtime';

function App() {
  return _jsx('h1', { children: 'Hello world' });
}

可以看到分出了一个独立 react/jsx-runtime 来做和 React.createElement 相同的事情。当然 React.createElement 还是可用的。

官方文档:

https://reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html

FiberNode

React Fiber 是 React 16 做的的新架构,旨在提高用户体验。

Fiber 是纤程的意思,一种任务调度的方式。React Fiber 通过时间分片的方式,实现一种并发的能力,将原本同步不可中断的大量更新,改成异步可中断更新,极大缓解了极端情况下的卡顿情况。

Fiber 架构中,最小的单位就是 FiberNode。它定义在 react-reconciler 包中。

FiberNode 的构造函数为:

代码语言:javascript复制
function FiberNode(
  tag: WorkTag,
  pendingProps: mixed,
  key: null | string,
  mode: TypeOfMode,
) {
  // 实例相关
  this.tag = tag;
  this.key = key;
  this.elementType = null;
  this.type = null;
  this.stateNode = null;

  // Fiber 相关
  this.return = null;
  this.child = null;
  this.sibling = null;
  this.index = 0;

  this.ref = null;

  this.pendingProps = pendingProps;
  this.memoizedProps = null;
  this.updateQueue = null;
  this.memoizedState = null;
  this.dependencies = null;

  this.mode = mode;

  // Effects 相关
  this.flags = NoFlags;
  this.subtreeFlags = NoFlags;
  this.deletions = null;

  this.lanes = NoLanes;
  this.childLanes = NoLanes;

  this.alternate = null;
  
  //...
}

实例相关属性

首先是和实例有关的属性:

1、tag 指定 FiberNode 类型,是一个数字。比如 0 代表 FunctionComponent,13 代表 SuspenseComponent。具体含义请看 源码文件:

https://github.com/facebook/react/blob/a6987bee730052dccdddd4645e15b1ce458fd9a6/packages/react-reconciler/src/ReactWorkTags.js#L38

代码语言:javascript复制
export const FunctionComponent = 0; // 函数组件
export const ClassComponent = 1; // 类组件
export const IndeterminateComponent = 2; // 不知道是函数组件还是类组件
export const HostRoot = 3; // 根节点 FiberRootNode,ReactDOM.render() 会产生该根节点。
export const HostPortal = 4; // ReactDOM.createPortal 产生的。
export const HostComponent = 5; // 原生组件,比如网页端对应的 div、span
export const HostText = 6; // 宿主文本节点
export const Fragment = 7; // React.Fragment
export const Mode = 8; // React.StrictMode
export const ContextConsumer = 9;
export const ContextProvider = 10;
export const ForwardRef = 11; // React.forwardRef 返回的组件
export const Profiler = 12;
export const SuspenseComponent = 13; // Suspense 组件
export const MemoComponent = 14; // React.memo 返回的组件
export const SimpleMemoComponent = 15;
export const LazyComponent = 16;
export const IncompleteClassComponent = 17;
export const DehydratedFragment = 18;
export const SuspenseListComponent = 19;
export const ScopeComponent = 21;
export const OffscreenComponent = 22; // 离屏组件
export const LegacyHiddenComponent = 23;
export const CacheComponent = 24;
export const TracingMarkerComponent = 25;

2、key 就是 <div key={9} /> 的 key,用于标识区分组件,减少不必要的销毁重渲染。

3、elementType 表示对应的组件,类似 ReactElement 的 type,值可能为 "div"、类函数或类函数本身。

4、type 基本和 elementType 类似,但多了 Symbol(react.offscreen) 这些 React 内置的特殊类型 symbol 值。

5、stateNode:对应的真实 DOM 节点,或 组件实例(比如是个函数组件或类组件)

fiber 树结构相关属性

然后是 fiber 链表指向相关的属性:

1、return:父节点

2、child:第一个子节点

3、sibling:下一个兄弟节点

4、index:在兄弟节点的位置

babel 怎么编译 jsx 的?

代码语言:javascript复制
const fs = require("fs");
const babel = require("@babel/core");

// 读取文件
fs.readFile("./component.js", (err, data) => {
  const code = data.toString("utf-8");
  
  // 使用 babel 的转换 API
  const result = babel.transformSync(code, {
    plugins: [
      [
        "@babel/plugin-transform-react-jsx",
        {
          // 使用自动引入 react/jsx-runtime 模式
          runtime: "automatic",
        },
      ],
    ],
  });
  /* result.code 就是转换后的代码,把它导出 */
  fs.writeFile("./dist/element.js", result.code, function () {});
});

和我们配置 babel 差不多,指定插件名和它的配置项,只是这里变成了 node.js 写法。

结尾

我是前端西瓜哥,欢迎关注我,学习更多前端知识。


0 人点赞