什么是 JSX ?
我们使用 React 时最大的感受是它让可以用一种更直观、更声明式的方式来描述用户界面,即 JSX。JSX 的语法类似于 HTML,但它并不是 HTML,而是 JavaScript 的一种语法扩展。
JSX 的本质
既然 JSX 并不是标准的三大件(HTML、CSS、JavaScript),那么它必然需要被转换(Transform)为标准的 JavaScript 代码才能在浏览器中运行。最常用的转换包括:
// 使用 @babel/preset-react 预设
{
"presets": ["@babel/preset-react"]
}
// tsconfig.json
{
"compilerOptions": {
"jsx": "react"
}
}
esbuild.build({
entryPoints: ["app.jsx"],
bundle: true,
outfile: "out.js",
jsx: "transform",
});
- SWC (Speedy Web Compiler):用 Rust 编写的快速 JavaScript/TypeScript 编译器,支持 JSX 转换
// .swcrc
{
"jsc": {
"parser": {
"syntax": "ecmascript",
"jsx": true
},
"transform": {
"react": {
"pragma": "React.createElement",
"pragmaFrag": "React.Fragment"
}
}
}
}
Babel Transform
本节以 Babel 为准,其他编译器处理方式类似。
@babel/preset-react
包含两种 runtime:
automatic
:自动选择 runtime
classic
:经典 runtime
目前来说,automatic
是主流的 runtime,无需手动引入 React
。
以如下的代码为例:
function Child() {
return <div>Child</div>;
}
function App() {
const element = <h1 style={{ color: "red" }}>Hello, world!</h1>;
console.log("🚀 ~ element:", element);
return (
<div>
{element} <Child />
</div>
);
}
会被转换为:
// 自动引入 jsx 和 jsxs 函数
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
function Child() {
return /*#__PURE__*/ _jsx("div", {
children: "Child",
});
}
function App() {
const element = /*#__PURE__*/ _jsx("h1", {
style: {
color: "red",
},
children: "Hello, world!",
});
console.log("🚀 ~ element:", element);
return /*#__PURE__*/ _jsxs("div", {
children: [element, " ", /*#__PURE__*/ _jsx(Child, {})],
});
}
而采用的 classic
runtime,还必须手动引入 React
,否则会报错。
import React from "react";
转换结果如下:
// 与 automatic 的区别,需要引入 React,否则 React.createElement 会报错
function Child() {
return /*#__PURE__*/ React.createElement("div", null, "Child");
}
function App() {
const element = /*#__PURE__*/ React.createElement(
"h1",
{
style: {
color: "red",
},
},
"Hello, world!"
);
console.log("🚀 ~ element:", element);
return /*#__PURE__*/ React.createElement(
"div",
null,
element,
" ",
/*#__PURE__*/ React.createElement(Child, null)
);
}
在开发模式下,指定 development
为 true
,可以提供开发所需要的调试信息。
{
"presets": [
[
"@babel/preset-react",
{
"development": true,
"runtime": "automatic"
}
]
]
}
代码转换结果如下:
var _jsxFileName = "xxx.jsx";
import { jsxDEV as _jsxDEV } from "react/jsx-dev-runtime";
function Child() {
return /*#__PURE__*/ _jsxDEV(
"div",
{
children: "Child",
},
void 0,
false,
{
fileName: _jsxFileName,
lineNumber: 2,
columnNumber: 10,
},
this
);
}
function App() {
const element = /*#__PURE__*/ _jsxDEV(
"h1",
{
style: {
color: "red",
},
children: "Hello, world!",
},
void 0,
false,
{
fileName: _jsxFileName,
lineNumber: 5,
columnNumber: 19,
},
this
);
console.log("🚀 ~ element:", element);
return /*#__PURE__*/ _jsxDEV(
"div",
{
children: [
element,
" ",
/*#__PURE__*/ _jsxDEV(
Child,
{},
void 0,
false,
{
fileName: _jsxFileName,
lineNumber: 9,
columnNumber: 17,
},
this
),
],
},
void 0,
true,
{
fileName: _jsxFileName,
lineNumber: 8,
columnNumber: 5,
},
this
);
}
babel playground
无论是 automatic
还是 classic
。本质都是将 JSX 转换成为函数调用,而函数执行后最终会返回一个 ReactElement
的数据结构。
比如上面的 element
就是 ReactElement
的数据结构,大概长这样:
React 19 对 JSX 的升级
React 19(包括 18 ) 对 JSX 的升级提案主要体现在以下几个方面:
- 废弃"模块模式"组件
- 废弃函数组件上的
defaultProps
- 废弃从对象中展开
key
- 废弃字符串
refs
(并移除生产模式的 _owner
字段)
- 将
ref
提取移至类组件 render 时机和 forwardRef
render 时机
- 将
defaultProps
解析移至类组件 render 时机
- 更改 JSX 转译器以使用新的元素创建方法
- 始终将
children
作为 props 传递
- 将
key
与其他 props 分开传递
- 在开发环境中
- 传递一个标志来确定是否为静态
- 将
__source
和 __self
与其他 props 分开传递
目标是最终移除对 forwardRef
的需求,使元素创建变得简单:
function jsx(type, props, key) {
return {
$$typeof: ReactElementSymbol,
type,
key,
props,
};
}
升级提案需要两方面的配合:
- Babel 对 JSX 处理的升级
- React 对 JSX 转换函数的逻辑升级
Babel 对 JSX 处理的升级
runtime 为automatic
在开发模式下,即:使用 @babel/plugin-transform-react-jsx-development
。会将绝大部分 JSX 转换为 jsxDEV
函数调用。
function App() {
return (
<h1 key="key" style={{ color: "red" }}>
title
</h1>
);
}
// 自动引入 jsxDEV 函数
import { jsxDEV as _jsxDEV } from "react/jsx-dev-runtime";
const element = /*#__PURE__*/ _jsxDEV(
"h1",
{
style: {
color: "red",
},
children: "title ",
},
"key",
false,
{
fileName: _jsxFileName,
lineNumber: 14,
columnNumber: 15,
},
this
);
jsxDEV
函数类型:
type jsxDEV = (
type,
config,
maybeKey,
isStaticChildren,
source,
self
) => ReactElement;
接收 6 个参数:
type
:元素类型
config
:即 props 对象,包含 children
等属性
maybeKey
:单独剥离的 key
属性
isStatic
:children
是否为静态
source
:源码信息
self
:当前组件实例,通常直接传入 this
,在类组件才有意义
type
包含以下几种类型:
- 字符串:表示原生 DOM 组件,如
div
- 函数:表示类、函数组件
- Symbol(或者特殊字符串 polyfill,主要针对React 内置组件):如
Fragment
,StrictMode
等
Babel 会根据 type
的类型,选择不同的转换逻辑。如果是原生标签(以小写开头的字符串),会转换为 string
类型,否则保持原样。
function App() {
const element1 = <div />;
const element2 = <span />;
const element3 = <Test />;
const element4 = <component.Test />;
const element5 = <React.StrictMode></React.StrictMode>;
const element6 = <></>;
}
会被转换为:
import {
jsxDEV as _jsxDEV,
Fragment as _Fragment,
} from "react/jsx-dev-runtime";
function App() {
const element1 = /*#__PURE__*/ _jsxDEV(
"div"
/** ... */
);
const element2 = /*#__PURE__*/ _jsxDEV(
"span"
/** ... */
);
const element3 = /*#__PURE__*/ _jsxDEV(
Test
/** ... */
);
const element4 = /*#__PURE__*/ _jsxDEV(
component.Test
/** ... */
);
const element5 = /*#__PURE__*/ _jsxDEV(
React.StrictMode
/** ... */
);
const element6 = /*#__PURE__*/ _jsxDEV(_Fragment /** ... */);
}
TIP
这也是为什么 React 希望组件名以大写字母开头,因为这样可以避免与原生标签混淆而导致 Babel 转换错误。
config
对象,即:对应 props 属性,会将除了 key
之外的所有属性收敛到其中,包含 children
属性(children
在以前的版本中会解析为 jsxDev
单独的参数 )。
const element = (
<h1 key="key" ref={ref} other={other}>
title
</h1>
);
会被转换为:
const element = /*#__PURE__*/ _jsxDEV(
"h1",
{
ref: ref,
other: other,
children: "title",
},
"key", // key 会被单独提出来,便于提高后续 React 解析性能
false,
{
fileName: _jsxFileName,
lineNumber: 5,
columnNumber: 15,
},
this
);
maybeKey
用于单独提取 key
属性,为什么会叫 maybeKey
呢?
比如以下场景:
const element = (
<h1 key="key" {...props}>
title
</h1>
);
props
里面说不定本身就包含 key
属性,而单纯从静态分析,Babel 无法得知这一点。
虽然如此,上面的代码依旧会被解析成:
const element = /*#__PURE__*/ _jsxDEV(
"h1",
{
...props,
children: "title",
},
"key",
false,
{
fileName: _jsxFileName,
lineNumber: 5,
columnNumber: 15,
},
this
);
INFO
现阶段,React 会在运行时依旧从 config
中解析 key
属性,但是会抛出 warning 信息,提示 key
应该被单独指定出来。下一步(不能确定什么时候)会停止从 config
中解析 key
现阶段还有一个问题:
An unresolved issue is how we distinguish <div key="Hi" {...props} />
from <div {...props} key="Hi" />
which currently have different semantics depending on if props has a key.
如果是:
const element = (
<h1 {...props} key="key">
title
</h1>
);
会被转换为:
import { createElement as _createElement } from "react";
const element = /*#__PURE__*/ _createElement(
"h1",
{
...props,
key: "key",
__self: this,
__source: {
fileName: _jsxFileName,
lineNumber: 2,
columnNumber: 15,
},
},
"title"
);
Babel 会使用 React.createElement
来代替 jsxDEV
函数。
NOTE
对这样的处理,笔者也有点疑惑。按理说,相比较于<div key="Hi" {...props} />
, <div {...props} key="Hi" />
更应该解析为jsxDEV("div",config,"key")
。
Babel 的对此的编译行为未来可能会有所改变,在此之前,将
key
放在前面,避免解析为
React.createElement
,能获得更好性能。
isStatic
用于标记 children
是否为静态。静态意味着 children
存在,且数量是固定的。比如:
const noChildren = <div />;
const oneChild = <div>Hi</div>;
const multipleChildren = (
<div>
<span>Hi</span>
<span>Hi</span>
</div>
);
const multipleChildren1 = (
<div>
{getChild()}
{someCondition ? getChild1() : getChild2()}
</div>
);
const dynamicChildren = (
<div>
{items.map((item) => (
<span key={item.id}>{item.name}</span>
))}
</div>
);
会被转换为:
const noChildren = /*#__PURE__*/ _jsxDEV(
"div",
{},
void 0,
false
/** ... */
);
const oneChild = /*#__PURE__*/ _jsxDEV(
"div",
{
children: "Hi",
},
void 0,
false
/** ... */
);
const multipleChildren = /*#__PURE__*/ _jsxDEV(
"div",
{
children: [
/*#__PURE__*/ _jsxDEV(
"span",
{
children: "Hi",
},
void 0,
false
/** ... */
),
/*#__PURE__*/ _jsxDEV(
"span",
{
children: "Hi",
},
void 0,
false
/** ... */
),
],
},
void 0,
true
/** ... */
);
const multipleChildren1 = /*#__PURE__*/ _jsxDEV(
"div",
{
children: [getChild(), someCondition ? getChild1() : getChild2()],
},
void 0,
true
/** ... */
);
const dynamicChildren = /*#__PURE__*/ _jsxDEV(
"div",
{
children: items.map((item) =>
/*#__PURE__*/ _jsxDEV(
"span",
{
children: item.name,
},
item.id,
false
/** ... */
)
),
},
void 0,
false
/** ... */
);
isStatic
和 Babel 对config.children
的解析相关联:
- 不存在
children
时,config
中也不存在 children
属性,isStatic
为 false
- 只存在一个
children
时,config
中虽然存在 children
属性,但不是数组类型,isStatic
为 false
- 存在多个
children
时,config
中存在 children
属性,且是数组类型
- 如果
children
中不是以map
函数形式存在,数量可数,isStatic
为 true
, 否则 isStatic
为 false
简单来说,当config.children
被解析为数组类型时,且数量是静态,可数的。isStatic
为 true
。
NOTE
isStatic
属性名会让人误解为:children
是完全不可变的。比如 div>Hi</div>
或者 <div><span color="red"/></div>
这样,而不是包含动态属性,比如 <div>{getChild()}</div>
或者 <div><span color={dynamic}/></div>
。
现阶段,Babel 主要是从
children
的个数来判断是否为静态。
在生产模式下,即使用 @babel/plugin-transform-react-jsx
。会将绝大部分 JSX 转换为 jsx
或者 jsxs
函数调用。
类型如下:
type jsx = (type, config, maybeKey) => ReactElement;
type jsxs = jsx;
相比于开发环境会少isStatic
, 和source
, self
等用于调试信息的参数。
type
, config
,maybeKey
转换方式和开发模式类似,jsx
和jsxs
的区别在于:
- 如果在开发模式下,
isStatic
被解析为 true
的情况,生产模式会转换为 jsxs
函数调用
- 如果在开发模式下,
isStatic
被解析为 false
的情况,生产模式会转换为 jsx
函数调用
还是以这段代码举例:
const noChildren = <div />;
const oneChild = <div>Hi</div>;
const multipleChildren = (
<div>
<span>Hi</span>
<span>Hi</span>
</div>
);
const multipleChildren1 = (
<div>
{getChild()}
{someCondition ? getChild1() : getChild2()}
</div>
);
const dynamicChildren = (
<div>
{items.map((item) => (
<span key={item.id}>{item.name}</span>
))}
</div>
);
会被转换为:
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
const noChildren = /*#__PURE__*/ _jsx("div", {});
const oneChild = /*#__PURE__*/ _jsx("div", {
children: "Hi",
});
const multipleChildren = /*#__PURE__*/ _jsxs("div", {
children: [
/*#__PURE__*/ _jsx("span", {
children: "Hi",
}),
/*#__PURE__*/ _jsx("span", {
children: "Hi",
}),
],
});
const multipleChildren1 = /*#__PURE__*/ _jsxs("div", {
children: [getChild(), someCondition ? getChild1() : getChild2()],
});
const dynamicChildren = /*#__PURE__*/ _jsx("div", {
children: items.map((item) =>
/*#__PURE__*/ _jsx(
"span",
{
children: item.name,
},
item.id
)
),
});
INFO
上文提到的开发模式中,会将<div {...props} key="Hi" />
转换为 React.createElement
的行为。在生产模式依旧存在。
runtime 为classic
当 runtime 为 classic
时,无论是开发模式,还是生产模式,所有的 JSX 都会转换 React.createElement
函数。
React.createElement
类型:
type createElement = (type, config, children) => ReactElement;
type
转换方式和 runtime
为 automatic
时一致
config
中包含除 children
的所有 props,包含 key
,ref
children
会被单独提出来,从第三个参数开始,平铺传入,后续的入参都会被视为是 children
的一项
比如:
import React from "react";
function Child() {
return <div>Child</div>;
}
function App() {
const element = (
<h1 key={"key"} style={{ color: "red" }} {...props}>
Hello, world!
</h1>
);
return (
<div {...props} key={"key"}>
{element} <Child />
</div>
);
}
开发模式下,会被转换为:
function _extends() {
return (
(_extends = Object.assign
? Object.assign.bind()
: function (n) {
for (var e = 1; e < arguments.length; e++) {
var t = arguments[e];
for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]);
}
return n;
}),
_extends.apply(null, arguments)
);
}
import React from "react";
function Child() {
return /*#__PURE__*/ React.createElement(
"div",
{
__self: this,
__source: {
fileName: _jsxFileName,
lineNumber: 13,
columnNumber: 10,
},
},
"Child"
);
}
function App() {
const element = /*#__PURE__*/ React.createElement(
"h1",
_extends(
{
key: "key",
style: {
color: "red",
},
},
props,
{
__self: this,
__source: {
fileName: _jsxFileName,
lineNumber: 17,
columnNumber: 5,
},
}
),
"Hello, world!"
);
return /*#__PURE__*/ React.createElement(
"div",
_extends({}, props, {
key: "key",
__self: this,
__source: {
fileName: _jsxFileName,
lineNumber: 23,
columnNumber: 5,
},
}),
// 从第三个参数开始,后续的入参都会被视为是 children 的一项
element,
" ",
/*#__PURE__*/ React.createElement(Child, {
__self: this,
__source: {
fileName: _jsxFileName,
lineNumber: 24,
columnNumber: 17,
},
})
);
}
生产模式下,会被转换为:
function _extends() {
return (
(_extends = Object.assign
? Object.assign.bind()
: function (n) {
for (var e = 1; e < arguments.length; e++) {
var t = arguments[e];
for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]);
}
return n;
}),
_extends.apply(null, arguments)
);
}
import React from "react";
function Child() {
return /*#__PURE__*/ React.createElement("div", null, "Child");
}
function App() {
const element = /*#__PURE__*/ React.createElement(
"h1",
_extends(
{
key: "key",
style: {
color: "red",
},
},
props
),
"Hello, world!"
);
return /*#__PURE__*/ React.createElement(
"div",
_extends({}, props, {
key: "key",
}),
// 从第三个参数开始,后续的入参都会被视为是 children 的一项
element,
" ",
/*#__PURE__*/ React.createElement(Child, null)
);
}
开发模式相比于生产模式,也仅仅多了一些调试信息,比如 config.__self
, config.__source
。
React 对 JSX 转换函数的逻辑升级
jsxDEV 函数
先从最复杂开发模式下的 runtime 为 automatic
的情况开始。从上可知,JSX 会被转换成jsxDEV
函数调用。
jsxDEV
核心功能包括:
- 类型检查:
- 验证 JSX 元素的类型是否有效
- 如果类型无效,会给出详细的错误信息
- 子元素处理:
- 验证子元素的 key 属性
- 处理静态子元素和动态子元素
- 属性处理:
- 处理 key 和 ref 属性
- 警告不正确的 key 使用方式
- 创建 props 对象,移除保留属性(如 key 和 ref)
- 默认属性处理:
- 如果组件有 defaultProps,不会再将其合并到 props 中
- 类组件会在 render 阶段才真正的处理 defaultProps
- 创建 React 元素:
- 最后调用 ReactElement 函数创建 React 元素
INFO
jsxDEV
函数代码中包含许多仅在开发环境(DEV)下运行的检查和警告。这些检查用于帮助开发者发现潜在的问题,如类型错误、key 使用不当等。
下方给出的源码中只会保留相对重要的主链路逻辑,省略了部分校验的逻辑
react/packages/react/src/jsx/ReactJSXElement.js
/**
* JSX 转换的入口函数,它调用 jsxDEVImpl 来实际处理 JSX 转换
*
* https://github.com/reactjs/rfcs/pull/107
*/
export function jsxDEV(type, config, maybeKey, isStaticChildren, source, self) {
return jsxDEVImpl(
type,
config,
maybeKey,
isStaticChildren,
source,
self,
// 开发环境下的特性开关,主要用于获取错误堆栈和优化异步调用栈的 debug
__DEV__ && enableOwnerStacks ? Error("react-stack-top-frame") : undefined,
__DEV__ && enableOwnerStacks ? createTask(getTaskName(type)) : undefined
);
}
function jsxDEVImpl(
type,
config,
maybeKey,
isStaticChildren,
source,
self,
debugStack,
debugTask
) {
if (__DEV__) {
/**
* 🚧 省略掉对 type,children key 的检查和警告 🚧
*/
// 现阶段,依旧会从 config 中获取 key 值,但是为下一阶段做准备,会警告 key 使用不当
// Warn about key spread regardless of whether the type is valid.
if (hasOwnProperty.call(config, "key")) {
const componentName = getComponentNameFromType(type);
const keys = Object.keys(config).filter((k) => k !== "key");
const beforeExample =
keys.length > 0
? "{key: someKey, " + keys.join(": ..., ") + ": ...}"
: "{key: someKey}";
if (!didWarnAboutKeySpread[componentName + beforeExample]) {
const afterExample =
keys.length > 0 ? "{" + keys.join(": ..., ") + ": ...}" : "{}";
console.error(
'A props object containing a "key" prop is being spread into JSX:\n' +
" let props = %s;\n" +
" <%s {...props} />\n" +
"React keys must be passed directly to JSX without using spread:\n" +
" let props = %s;\n" +
" <%s key={someKey} {...props} />",
beforeExample,
componentName,
afterExample,
componentName
);
didWarnAboutKeySpread[componentName + beforeExample] = true;
}
}
let key = null;
let ref = null;
// 下面注释提到了对 <div {...props} key="Hi" or <div key="Hi" {...props} /> 两种情况的处理还有点问题
// Currently, key can be spread in as a prop. This causes a potential
// issue if key is also explicitly declared (ie. <div {...props} key="Hi" />
// or <div key="Hi" {...props} /> ). We want to deprecate key spread,
// but as an intermediary step, we will use jsxDEV for everything except
// <div {...props} key="Hi" />, because we aren't currently able to tell if
// key is explicitly declared to be undefined or not.
if (maybeKey !== undefined) {
if (__DEV__) {
checkKeyStringCoercion(maybeKey);
}
key = "" + maybeKey;
}
// 依旧会从 config 中获取 key 值
if (hasValidKey(config)) {
if (__DEV__) {
checkKeyStringCoercion(config.key);
}
key = "" + config.key;
}
// 从 config 中获取 ref 值, 在 19 中,enableRefAsProp 和 disableStringRefs 都是 true
// 下面的逻辑不会进入,不用关心
if (hasValidRef(config)) {
if (!enableRefAsProp) {
ref = config.ref;
if (!disableStringRefs) {
ref = coerceStringRef(ref, getOwner(), type);
}
}
if (!disableStringRefs) {
warnIfStringRefCannotBeAutoConverted(config, self);
}
}
let props;
// const enableFastJSXWithoutStringRefs = enableRefAsProp && disableStringRefs;
// 所以第一个条件肯定是满足的,只需要关心 `!("key" in config)` 的情况
if (
(enableFastJSXWithoutStringRefs ||
(enableRefAsProp && !("ref" in config))) &&
!("key" in config)
) {
// 性能优化措施,尽可能重用原始的 props 对象
// If key was not spread in, we can reuse the original props object. This
// only works for `jsx`, not `createElement`, because `jsx` is a compiler
// target and the compiler always passes a new object. For `createElement`,
// we can't assume a new object is passed every time because it can be
// called manually.
//
// Spreading key is a warning in dev. In a future release, we will not
// remove a spread key from the props object. (But we'll still warn.) We'll
// always pass the object straight through.
props = config;
} else {
// We need to remove reserved props (key, prop, ref). Create a fresh props
// object and copy over all the non-reserved props. We don't use `delete`
// because in V8 it will deopt the object to dictionary mode.
props = {};
for (const propName in config) {
// Skip over reserved prop names
if (propName !== "key" && (enableRefAsProp || propName !== "ref")) {
// ref 不会再作为保留属性,而是会保留在 props 中
if (enableRefAsProp && !disableStringRefs && propName === "ref") {
props.ref = coerceStringRef(config[propName], getOwner(), type);
} else {
props[propName] = config[propName];
}
}
}
}
// 在 19 中,disableDefaultPropsExceptForClasses 为 true
// 所以不会再将 defaultProps 合并到 props 中
if (!disableDefaultPropsExceptForClasses) {
// Resolve default props
if (type && type.defaultProps) {
const defaultProps = type.defaultProps;
for (const propName in defaultProps) {
if (props[propName] === undefined) {
props[propName] = defaultProps[propName];
}
}
}
}
// 最后调用 ReactElement 函数创建 React 元素
return ReactElement(
type,
key,
ref,
self,
source,
getOwner(),
props,
debugStack,
debugTask
);
}
}
react/packages/shared/ReactFeatureFlags.js
// 19 中上面涉及到的几个特性开关都是 true
// Passes `ref` as a normal prop instead of stripping it from the props object
// during element creation.
export const enableRefAsProp = true;
export const disableStringRefs = true;
// TODO: Land at Meta before removing.
export const disableDefaultPropsExceptForClasses = true;
react/packages/react/src/jsx/ReactJSXElement.js
function ReactElement(
type,
key,
_ref,
self,
source,
owner,
props,
debugStack,
debugTask
) {
let ref;
if (enableRefAsProp) {
// When enableRefAsProp is on, ignore whatever was passed as the ref
// argument and treat `props.ref` as the source of truth. The only thing we
// use this for is `element.ref`, which will log a deprecation warning on
// access. In the next release, we can remove `element.ref` as well as the
// `ref` argument.
const refProp = props.ref;
// An undefined `element.ref` is coerced to `null` for
// backwards compatibility.
ref = refProp !== undefined ? refProp : null;
} else {
ref = _ref;
}
// 分不同的情况构建 element,主要区别在于 ref 的处理
let element;
if (__DEV__ && enableRefAsProp) {
// In dev, make `ref` a non-enumerable property with a warning. It's non-
// enumerable so that test matchers and serializers don't access it and
// trigger the warning.
//
// `ref` will be removed from the element completely in a future release.
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,
key,
props,
// Record the component responsible for creating this element.
_owner: owner,
};
if (ref !== null) {
Object.defineProperty(element, "ref", {
enumerable: false,
get: elementRefGetterWithDeprecationWarning,
});
} else {
// Don't warn on access if a ref is not given. This reduces false
// positives in cases where a test serializer uses
// getOwnPropertyDescriptors to compare objects, like Jest does, which is
// a problem because it bypasses non-enumerability.
//
// So unfortunately this will trigger a false positive warning in Jest
// when the diff is printed:
//
// expect(<div ref={ref} />).toEqual(<span ref={ref} />);
//
// A bit sketchy, but this is what we've done for the `props.key` and
// `props.ref` accessors for years, which implies it will be good enough
// for `element.ref`, too. Let's see if anyone complains.
Object.defineProperty(element, "ref", {
enumerable: false,
value: null,
});
}
} else if (!__DEV__ && disableStringRefs) {
// In prod, `ref` is a regular property and _owner doesn't exist.
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,
key,
ref,
props,
};
} else {
// In prod, `ref` is a regular property. It will be removed in a
// future release.
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,
key,
ref,
props,
// Record the component responsible for creating this element.
_owner: owner,
};
}
/**
* 🚧 省略掉对 element 的开发时 debug 属性的处理 🚧
*/
return element;
}
经过上面的流程,函数jsxDEV
将编译后 JSX 对象转换为 ReactElement 元素,这个元素是 React 内部用于描述组件树的节点,作为 React 渲染和更新机制的输入。
性能提升
Babel 会将 JSX 的 props 使用一个新的对象来承载,在以前的版本中,jsxDEV
总是会创建一个新的对象,用于承载剔除掉 ref
,key
等保留属性,而现在,会尽可能重用这个对象,以减少内存分配和垃圾回收的负担。
性能问题
生产模式下的 jsx
& jsxs
函数本质就是jsxProd
函数,即 jsxDEV
函数去除了校验逻辑的版本,不再赘述。
TIP
ReactElement 元素 和 Fiber 节点是不同的,后文会详解介绍 Fiber 节点,注意不要混淆。
createElement 函数
上文提到 automatic
模式下有些特殊情况,或者是 classic
模式下,JSX 会被转换成 createElement
函数调用。
下面来看下 createElement
函数的实现。
react/packages/react/src/jsx/ReactJSXElement.js
export function createElement(type, config, children) {
/**
* 🚧 省略掉对 type,children key 的检查和警告 🚧
*/
let propName;
// Reserved names are extracted
const props = {};
let key = null;
let ref = null;
// config 中包含除 children 的所有 props,包含 key,ref
if (config != null) {
if (hasValidRef(config)) {
if (!enableRefAsProp) {
ref = config.ref;
if (!disableStringRefs) {
ref = coerceStringRef(ref, getOwner(), type);
}
}
if (__DEV__ && !disableStringRefs) {
warnIfStringRefCannotBeAutoConverted(config, config.__self);
}
}
if (hasValidKey(config)) {
if (__DEV__) {
checkKeyStringCoercion(config.key);
}
key = "" + config.key;
}
// 将 config 中除 key 之外的属性添加到 props 对象中,由于每次都会构建新的 props 对象,性能会差点
// Remaining properties are added to a new props object
for (propName in config) {
if (
hasOwnProperty.call(config, propName) &&
// Skip over reserved prop names
propName !== "key" &&
(enableRefAsProp || propName !== "ref") &&
// Even though we don't use these anymore in the runtime, we don't want
// them to appear as props, so in createElement we filter them out.
// We don't have to do this in the jsx() runtime because the jsx()
// transform never passed these as props; it used separate arguments.
propName !== "__self" &&
propName !== "__source"
) {
if (enableRefAsProp && !disableStringRefs && propName === "ref") {
props.ref = coerceStringRef(config[propName], getOwner(), type);
} else {
props[propName] = config[propName];
}
}
}
}
// 从第三个参数开始,后续的入参都会被视为是 children 的一项,将其收敛到 props 属性中
// 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];
}
if (__DEV__) {
if (Object.freeze) {
Object.freeze(childArray);
}
}
props.children = childArray;
}
// 与 jsxDEV 不同的是,少了特性开关,所以组件的 defaultProps,总是会被合并到 props 中
// Resolve default props
if (type && type.defaultProps) {
const defaultProps = type.defaultProps;
for (propName in defaultProps) {
if (props[propName] === undefined) {
props[propName] = defaultProps[propName];
}
}
}
return ReactElement(
type,
key,
ref,
undefined,
undefined,
getOwner(),
props,
__DEV__ && enableOwnerStacks ? Error("react-stack-top-frame") : undefined,
__DEV__ && enableOwnerStacks ? createTask(getTaskName(type)) : undefined
);
}
TIP
我们偶尔可能也会用到 React.cloneElement
函数,它主要用于克隆 ReactElement 元素,并进行一些扩展。核心代码流程和 React.createElement
函数大致一致,不再赘述。
参考