在准备步入 Reconciler 阶段之前,先了解下 JSX 将会如何被编译,并且编译之后的对象会有哪些属性。
JSX to Fiber
在 Babel 官网中,下面这个例子
function hello() {
return <div id="hello">
<h1 className="title">This is Header</h1>
<span>This is Content</span>
<a>link</a>
</div>
}
会被编译为:
function hello() {
return /*#__PURE__*/ React.createElement(
"div",
{
id: "hello"
},
/*#__PURE__*/ React.createElement(
"h1",
{
className: "title"
},
"This is Header"
),
/*#__PURE__*/ React.createElement("span", null, "This is Content"),
/*#__PURE__*/ React.createElement("a", null, "link")
);
}
那么 createElement 又做了什么呢?
在 react/packages/react/src/React.js 中定义了:
import {
createElement as createElementProd,
...
} from './ReactElement';
...
const createElement = __DEV__ ? createElementWithValidation : createElementProd;
也就是说,在开发模式调用 createElementWithValidation,在生产中调用 createElementProd。
createElementProd
存在 3 个参数
type:即例子中的div、h1、span、a标签config:即{id: "hello"}等属性children:若单一节点则是This is Content、link,若多个则转化为列表
export function createElement(type, config, children) {
let propName;
// Reserved names are extracted
const props = {};
let key = null;
let ref = null;
let self = null;
let source = null;
if (config != null) {
...
// Remaining properties are added to a new props object
for (propName in config) {
...
props[propName] = config[propName];
...
}
}
// Children can be more than one argument, and those are transferred onto
// the newly allocated props object.
const childrenLength = arguments.length - 2;
if (childrenLength === 1) {
props.children = children;
} else if (childrenLength > 1) {
const childArray = Array(childrenLength);
for (let i = 0; i < childrenLength; i++) {
childArray[i] = arguments[i + 2];
}
...
props.children = childArray;
}
...
return ReactElement(
type,
key,
ref,
self,
source,
ReactCurrentOwner.current,
props,
);
}
- 第一步先对
第二个参数config进行遍历,拿到解析好的属性 - 第二步解析
第三个参数children。若是多参数,则直接childArray[i] = arguments[i + 2];解析。 - 第三步调用
ReactElement方法返回组装好的对象。
ReactElement
const ReactElement = function(type, key, ref, self, source, owner, props) {
const 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: type,
key: key,
ref: ref,
props: props,
// Record the component responsible for creating this element.
_owner: owner,
};
...
return element;
};
扩展阅读
Babel如何转化JSX
先引用一篇文章 全新的 JSX 转换 👈 (意译)
动机
React0.12 前后,我们对 key, ref, defaultProps 做了一些小改动。特别是,它们很早在调用 React.createElement(...) 的时候就已经解决了。一切都是类组件时,就很合理。但从那以后,我们介绍了函数组件。Hooks 也让函数组件更为流行。是时候重新评估一些设计去做一些简化的事儿了(至少对函数组件来说)
元素的创建很频繁,因为被大量使用并且在渲染的时候频繁创建。
React.createElement(...) 从来没有打算成为 JSX 的实现。但它是当时我们的最佳实践。它目的是你可以手动编写 (如果你不想使用 createFactory 表单)。替代品并没有提供足够的价值可以保证它们用在任何地方。这有很多问题:
- 如果组件在每个元素创建调用期间都有
.defaultProps,我们需要对它进行动态测试。
设计细节
设计分为3步:1. 全新的 JSX 转换,2. 弃用和警告 3. 实际语意断言
1. JSX 转换的不同
为了支持 React JSX 的变化,有很多编译、打包、下游工具等一系列组合需要升级。
2.1 自动导入
首先我们需要避免在作用域中引入 React
理想情况下,元素的创建应该是编译器运行时的一部分。有一些实际方便的考虑。首先,我们有 DEV 和 PROD 两种模式。DEV 模式版本要复杂得多,并且集成到 React 中。我们还在版本之间进行了细微的更改——比如这个。
相比较更新编译器的工作链来说,部署 npm 包来迭代新版本会更容易点。因此,把具体的实现放在 react 包中是最好的。
理想情况下,使用 JSX 时无需任何导入:
function Foo() {
return <div /;
}
然后编译的时候就包含这个依赖,随后,打包器就能把它变成想要的。
import {jsx} from "react";
function Foo() {
return jsx('div', ...);
}
问题是并不是所有的工具都支持从一个转换新增一个依赖。第一步是搞清楚,在当前生态中它是如何做到的。
2.2 把 key 从 props 中剥离出来
目前来说,key 作为 props 的一部分传递的,但未来我们想特殊处理。所以需要把它作为一个单独的参数传递。
jsx('div', props, key)
2.3 总是把 children 作为 props 传递
在 createElement,children 作为参数变量传递。在新的转换中,我们总是把它添加到 props 对象中。
我们用参数变量传值的原因是想在 DEV 模式中区分静态和动态 children。我提议是把 <div{a}{b}</div 编译成 jsxs('div', {children: [a, b]}),<div{a}</div 编译成 jsx('div', {children:a})。jsxs 函数表示上面的数组是 React 创建的。这个策略好的一点就是,即使你没有在 PROD 和 DEV 分离构建的步骤,我们仍然可以在 PROD 环境中发出关键警告,无需任何开销。
2.4 DEV only transforms
我们对于 DEV 有特别的转化。__source 和 __self 并不是 props 的一部分。我们可以用分离开的参数传值。
一个可行方案是做分离函数去编译 DEV
jsxDEV(type, props, key, isStaticChildren, source, self)
如果转换不匹配,我们很容易出错
2.5 Spread only
这种特殊的匹配:
<div {...props} />
可以安全的优化成:
createElement('div', props)
是因为 createElement() 总是会克隆传递进来的对象。在新的转化中,我们想要避免在 jsx() 函数中克隆。多数情况下不会被观察到,因为 JSX 总是会创建一个新的内联对象。
或者