大家好,我是前端西瓜哥。今天学习一下 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 包中,接受:
- 元素的 type;
- 组件的 props(包括 key 和 ref);
- 其余参数则是子元素,同样是 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"}
。此时会编译为类似下面的代码:
// 此行会在编译时自动引入
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 写法。
结尾
我是前端西瓜哥,欢迎关注我,学习更多前端知识。