开始
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
Loading
开发环境调用的函数版本包含很多的校验和错误提示,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
开始
React v17 增加了 jsx-runtime 机制,摒弃了过去通过
React.CreateElement创建 React element 的方式。以<h1>Poem</h1>为例,在 React v16 及其之前,上面的 JSX 会被转译成:而在 React v17 之后,通过正确的 transpiler 配置(以 tsc 为例,
compilerOptions.jsx的值需要是ReactJSX),会被转译成:乍一看似乎只是换了个函数,但是有几点需要引起注意。
首先是两个函数的签名。
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带来的好处主要有两个:_jsx比CreateElement更简短而且更可压缩替换,打包完成之后 JavaScript bundle 的体积将会稍微减少一些。_jsx 与 _jsxs
再来看一个在项目中非常常用的、动态创建 React element 的例子:
上面的代码会被转译成:
不仔细看的话可能会忽略
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:这段代码意图很清晰:如果
children是静态数组,对children中的每个元素进行key的校验;如果children不是数组,或者是动态数组,那么对整个children做校验。validateChildKeys函数会判断传入的children/child是否是数组,如果是数组则会校验数组中的每个元素是否有合法的key属性。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 都只是一个带有几个特殊属性的字面量对象而已:
下面这张图里表示的是

<button onClick={addCount}>Add</button>对应的 React element 对象:总结
_jsx和_jsxs的区别在于能够校验动态生成的children的key属性,在生产环境版本的 React 中,两者指向同一个函数。graph TD; _jsx/_jsxs --> jsxWithValidation --> jsx --> ReactElementdefaultProps是在jsx函数中 merge 到 props 中的:Quiz
判断下面 console.log 语句输出内容的顺序
Answer
顺序为: 1 3 2
其实只要能想象到 JSX 被转译成普通的 JavaScript 代码的样子,就能知道答案。
App组件被转译成:这里
_jsx(Button, { onClick: addCount })调用完之后只会返回一个 React element 对象,并没有执行Button函数。内容不错或者比较美观的文章
React 17 introduces new JSX transform
JSX.Element vs ReactElement vs ReactNode