1. jsx-runtime
开始
React v17 增加了 jsx-runtime 机制,摒弃了过去通过 React.CreateElement 创建 React element 的方式。以 <h1>Poem</h1> 为例,在 React v16 及其之前,上面的 JSX 会被转译成:
import React from 'React';
const a = React.createElement("h1", null, "Poem");
而在 React v17 之后,通过正确的 transpiler 配置(以 tsc 为例,compilerOptions.jsx 的值需要是 ReactJSX),会被转译成:
import { jsx as _jsx } from "react/jsx-runtime";
const a = _jsx("h1", { children: "Poem" });
乍一看似乎只是换了个函数,但是有几点需要引起注意。
首先是两个函数的签名。React.createElement 接收的参数为 (type, props, ...children), _jsx 接收的参数为 (type, config, maybeKey)。新的 _jsx 函数在创建 React element 的时候,把 children 属性放进了 props (config) 里,而把 key 独立了出来。
第二个变化是,原先 createElement 是 React 的一个方法,因此在写 JSX 的时候,我们通常还需要通过 import React from 'react' 在当前模块中引入 React。而在 React v17 之后,配合正确的 transpiler 配置,不再需要显示引入 React,transpiler 会自动根据配置引入对应的 jsx 函数。
由 React.createElement 改成 react/jsx-runtime 带来的好处主要有两个:
- 不再显示引入 React 到当前模块。
- 因为
_jsx比CreateElement更简短而且更可压缩替换,打包完成之后 JavaScript bundle 的体积将会稍微减少一些。
Warning
React.createElement方法并没有从代码里去掉,React v17 可以继续向下兼容。更何况我们还可以在代码里手动调用React.createElement来创建 React Element。
_jsx 与 _jsxs
再来看一个在项目中非常常用的、动态创建 React element 的例子:
const App = () => {
return (
<div className="outer">
<span>Header</span>
<div className="inner">
{[1, 2].map(num => (
<p>{num}</p>
))}
</div>
</div>
)
}
上面的代码会被转译成:
const App = () => {
return _jsxs(
'div',
Object.assign(
{ className: 'outer' },
{
children: [
_jsx('span', { children: 'Header' }),
_jsx(
'div',
Object.assign({ className: 'inner' }, { children: [1, 2].map(num => _jsx('p', { children: num })) })
),
],
}
)
)
}
不仔细看的话可能会忽略 div.outer 元素是通过 _jsxs 而不是 _jsx 创建的。_jsxs 和 _jsx 的区别在于,前者应该只对 children 为“静态”数组的元素调用,后者则是对元素的 children 属性为非数组或者只为“动态”数组时调用。
这里所谓的静态与动态,指的是子元素是否会在每次渲染时,存在动态排序、增删的情况。很明显,div.outer 具有两个在源码中就有固定顺序的子元素。而 div.inner 的子元素则是通过表达式动态生成的,每次 App 组件渲染时,div.inner 的子元素与上次相比可能交换过了顺序,或者删除、增加了某些子元素。
校验 key
我们都知道 React 要求开发者为每个动态生成的子元素手动增加一个 key 属性,相当于给这些子元素赋予一个固定的 ID,以便能够让 React 能够在同级元素中检测到哪些元素被移位、删除和新增。之所以上面的代码里会需要区别静态与动态子元素,并使用两个不同的方法来生成元素,就是为了校验子元素的 key 属性。
下面简单分析下具体的代码细节。两个函数内部都只调用了 jsxWithValidation 函数,区别在于传入的参数。先看看 jsxWithValidation 函数能接收的参数:(type, props, key, isStaticChildren, source, self)。_jsxs 和 _jsx 区别只在于传入的 isStaticChildren 的值,前者是 true,后者是 false。jsxWithValidation 中只在一个地方使用了 isStaticChildren:
if (isStaticChildren) {
if (isArray(children)) {
for (let i = 0; i < children.length; i++) {
validateChildKeys(children[i], type);
}
if (Object.freeze) {
Object.freeze(children);
}
} else {
console.error(
'React.jsx: Static children should always be an array. ' +
'You are likely explicitly calling React.jsxs or React.jsxDEV. ' +
'Use the Babel transform instead.',
);
}
} else {
validateChildKeys(children, type);
}
这段代码意图很清晰:如果 children 是静态数组,对 children 中的每个元素进行 key 的校验;如果 children 不是数组,或者是动态数组,那么对整个 children 做校验。validateChildKeys 函数会判断传入的 children/child 是否是数组,如果是数组则会校验数组中的每个元素是否有合法的 key 属性。
Note 在生产环境中,
_jsxs与_jsx其实都指向同一个函数:jsxProd, 省略了几乎所有的校验。
React Element
_jsxs 和 _jsx 函数其实更多的只是在开发环境中做一些校验,真正重要的是它的返回值。
前面我提到过几次 React element,这个概念似乎有点抽象又有点跟其他概念混淆。简单来讲的话,React element 就是 React component 的调用返回值。以上面的代码为例,App 是 React component,a 是 React element。
_jsxs 和 _jsx 除了校验一些参数之外,还调用了一个关键函数 jsx (开发环境的话是 jsxDEV。没错,jsxDEV 跟 jsx 的主要区别也是会做更多的校验),而 jsx 的返回值就是 React element。
每个 React element 都只是一个带有几个特殊属性的字面量对象而已:
// packages/shared/ReactElementType.js 里的 flow 类型定义
export type ReactElement = {
$$typeof: any,
type: any,
key: any,
ref: any,
props: any,
// ReactFiber
_owner: any,
// __DEV__
_store: {validated: boolean, ...},
_self: React$Element<any>,
_shadowChildren: any,
_source: Source,
};
下面这张图里表示的是 <button onClick={addCount}>Add</button> 对应的 React element 对象:

总结
_jsx和_jsxs的区别在于能够校验动态生成的children的key属性,在生产环境版本的 React 中,两者指向同一个函数。- 理一下调用关系:
graph TD;
_jsx/_jsxs --> jsxWithValidation --> jsx --> ReactElement
- 开发环境调用的函数版本包含很多的校验和错误提示,React 中其他地方的很多函数也是如此,这也是为什么 dev 环境的 React 应用性能比 prod 环境的要差很多。
- 函数式组件的
defaultProps是在jsx函数中 merge 到 props 中的:
// Resolve default props
if (type && type.defaultProps) {
const defaultProps = type.defaultProps;
for (propName in defaultProps) {
if (props[propName] === undefined) {
props[propName] = defaultProps[propName];
}
}
}
Quiz
判断下面 console.log 语句输出内容的顺序
const Button = (props) => {
console.log(2)
return <button onClick={props.onClick}>Add</button>
}
const App = () => {
const [count, setCount] = useState(0)
const addCount = () => {
setCount(count + 1)
}
return (
<div>
{console.log(1)}
Count: {count}
<Button onClick={addCount} />
{console.log(3)}
</div>
)
}
Answer
顺序为: 1 3 2
其实只要能想象到 JSX 被转译成普通的 JavaScript 代码的样子,就能知道答案。App 组件被转译成:
const App = () => {
const [count, setCount] = useState(0)
const addCount = () => {
setCount(count + 1)
}
return _jsxs('div', {
children: [console.log(1), 'Count: ', count, _jsx(Button, { onClick: addCount }), console.log(3)],
})
}
这里 _jsx(Button, { onClick: addCount }) 调用完之后只会返回一个 React element 对象,并没有执行 Button 函数。
内容不错或者比较美观的文章
React 17 introduces new JSX transform JSX.Element vs ReactElement vs ReactNode