JSX 元素节点会被编译成 React Element 形式:
React.createElement(
type,
[props],
[...children]
)
举个例子:
<div>
<TextComponent />
<div>hello,world</div>
let us learn React!
</div>
上面的代码会被 babel 先编译成:
React.createElement("div", null,
React.createElement(TextComponent, null),
React.createElement("div", null, "hello,world"),
"let us learn React!"
)
最终,在调和阶段,上述 React element 对象的每一个子节点都会形成一个与之对应的 fiber 对象,然后通过 sibling、return、child 将每一个 fiber 对象联系起来。
不同种类的 fiber Tag
React 针对不同 React element 对象会产生不同 tag (种类) 的fiber 对象:
export const FunctionComponent = 0; // 函数组件
export const ClassComponent = 1; // 类组件
export const IndeterminateComponent = 2; // 初始化的时候不知道是函数组件还是类组件
export const HostRoot = 3; // Root Fiber 可以理解为根元素 , 通过reactDom.render()产生的根元素
export const HostPortal = 4; // 对应 ReactDOM.createPortal 产生的 Portal
export const HostComponent = 5; // dom 元素 比如 <div>
export const HostText = 6; // 文本节点
export const Fragment = 7; // 对应 <React.Fragment>
export const Mode = 8; // 对应 <React.StrictMode>
export const ContextConsumer = 9; // 对应 <Context.Consumer>
export const ContextProvider = 10; // 对应 <Context.Provider>
export const ForwardRef = 11; // 对应 React.ForwardRef
export const Profiler = 12; // 对应 <Profiler/ >
export const SuspenseComponent = 13; // 对应 <Suspense>
export const MemoComponent = 14; // 对应 React.memo 返回的组件
jsx 最终形成的 fiber 结构图
最终写的 jsx 会变成如下格式:
fiber 对应关系
- child: 一个由父级 fiber 指向子级 fiber 的指针。
- return:一个子级 fiber 指向父级 fiber 的指针。
- sibling: 一个 fiber 指向下一个兄弟 fiber 的指针。
Babel 解析 JSX 流程
1 @babel/plugin-syntax-jsx 和 @babel/plugin-transform-react-jsx
JSX 语法实现来源于这两个 babel 插件:
- @babel/plugin-syntax-jsx : 使用这个插件,能够让 Babel 有效的解析 JSX 语法。
- @babel/plugin-transform-react-jsx :这个插件内部调用了 @babel/plugin-syntax-jsx,可以把 React JSX 转化成 JS 能够识别的 createElement 格式。
Automatic Runtime
新版本 React 已经不需要引入 createElement ,这种模式来源于 Automatic Runtime:
function Index(){
return <div>
<h1>hello,world</h1>
<span>let us learn React</span>
</div>
}
被编译后的文件:
js
复制代码
import { jsx as _jsx } from "react/jsx-runtime";
import { jsxs as _jsxs } from "react/jsx-runtime";
function Index() {
return _jsxs("div", {
children: [
_jsx("h1", {
children: "hello,world"
}),
_jsx("span", {
children:"let us learn React" ,
}),
],
});
}
plugin-syntax-jsx 已经向文件中提前注入了 _jsxRuntime api。不过这种模式下需要我们在 .babelrc 设置 runtime: automatic 。
json
复制代码
"presets": [
["@babel/preset-react",{
"runtime": "automatic"
}]
],
Classic Runtime
还有一个就是经典模式,在经典模式下,使用 JSX 的文件需要引入 React ,不然就会报错。
业务代码中写的 JSX 文件:
import React from 'react'
function Index(){
return <div>
<h1>hello,world</h1>
<span>let us learn React</span>
</div>
}
被编译后的文件:
import React from 'react'
function Index(){
return React.createElement(
"div",
null,
React.createElement("h1", null,"hello,world"),
React.createElement("span", null, "let us learn React")
);
}
2 api层面模拟实现
接下来我们通过 api 的方式来模拟一下 Babel 处理 JSX 的流程。
第一步:创建 element.js,写下将测试的 JSX 代码。
import React from 'react'
function TestComponent(){
return <p> hello,React </p>
}
function Index(){
return <div>
<span>模拟 babel 处理 jsx 流程。</span>
<TestComponent />
</div>
}
export default Index
第二步:因为 babel 运行在 node 环境,所以同级目录下创建 jsx.js 文件。来模拟一下编译的效果。
const fs = require('fs')
const babel = require("@babel/core")
/* 第一步:模拟读取文件内容。 */
fs.readFile('./element.js',(e,data)=>{
const code = data.toString('utf-8')
/* 第二步:转换 jsx 文件 */
const result = babel.transformSync(code, {
plugins: ["@babel/plugin-transform-react-jsx"],
});
/* 第三步:模拟重新写入内容。 */
fs.writeFile('./element.js',result.code,function(){})
})
element.js 变成了:
import React from 'react';
function TestComponent() {
return /*#__PURE__*/React.createElement("p", null, " hello,React ");
}
function Index() {
return /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("span", null, "\u6A21\u62DF babel \u5904\u7406 jsx \u6D41\u7A0B\u3002"), /*#__PURE__*/React.createElement(TestComponent, null));
}
export default Index;
如上可以看到已经成功转成 React.createElement 形式,从根本上弄清楚了 Babel 解析 JSX 的大致流程。