前言
什么叫JSX
?,我们可以先从 React 官网 的定义窥见一二。其中讲到JSX 可以很好地描述 UI 应该呈现出它应有交互的本质形式,即可以让我们写 React 组件像在写 HTML 一样。除了类似原生的HTML
标签外,我们还可以自定义自己的组件,进而一步步构成复杂的页面。那么在这一过程中React
是如何处理JSX
的,我们将在接下来一一拆解,透过现象来挖掘背后的本质。
本文源码 feature/jsx ,在scripts
中提供了三个命令:
## 需要import React,index.js
yarn dev or npm run dev
## 不需要import React,index-jsx-runtime.js
yarn runtime or npm run runtime
## 自定义jsx-runtime, index-feact-jsx-runtime.js
yarn feact or npm run feact
import React from 'react'
在React17
以前,如果我们不显式调用import React from 'react'
那么在页面上肯定会报如下错误
当然在我们刚开始用React
的时候,前辈、书里都说过必须在顶部显示import React from 'react'
,久而久之,我们都会条件反射式地写上这么一句。可是你有没有想过,为何必须这样做页面才不会报错警告呢?明明我下面都没用到React
相关的啊!
而且你还发现,每次你写上import React from 'react'
,即使发现代码里都没有用到React
相关的,但是vscode
里面的import React
就会高亮,而对比下面的import eslint
就是暗色的,并且提示声明了但没使用
诶,上面提到的两个问题为何那么诡异?下面,让我们化身福尔摩斯来一一揭秘吧~
React.createElement
React 官网 JSX 表示对象 有一句话:Babel 会把 JSX 转译成一个名为 React.createElement() 函数调用。翻译翻译,就是说我们写的组件中用到了JSX
,那么转译的过程中Babel
就会去找React.createElement
,将JSX
转译为相应的对象。
举个 🌰:
function App() {
return <div className='.app'>app</div>
}
console.log('App: ', App)
console.log('<App/>: ', <App/>)
上面如果直接输出 App,那么 App 本质上就是一个函数,而如果是这样的用法<App/>
,即调用了组件,我们可以在控制台看到看到两种的区别
即Babel
会将<App/>
转译为下面的代码:
React.createElement(
ƒ App(), // type, ƒ App()表示函数的意思
{}, // config
undefined // children
);
而之后转译div
,也会转译为下面代码:
React.createElement(
'div', // type
{ className: ".app" }, // config
'app' // children
);
那我们看下 createElement 的实现
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) {
// 1.这里验证了ref和key如果都符合要求,会拦截这两者
if (hasValidRef(config)) {
ref = config.ref;
}
if (hasValidKey(config)) {
key = '' + config.key;
}
// 以下省略了__self和__source
...
// 2.处理除了ref和key的props
for (propName in config) {
if (
hasOwnProperty.call(config, propName) &&
!RESERVED_PROPS.hasOwnProperty(propName)
) {
props[propName] = config[propName];
}
}
}
// 3.处理children
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;
}
// 4.处理defaultProps
if (type && type.defaultProps) {
const defaultProps = type.defaultProps;
for (propName in defaultProps) {
if (props[propName] === undefined) {
props[propName] = defaultProps[propName];
}
}
}
// 5.最后调用ReactElement
return ReactElement(
type,
key,
ref,
self,
source,
ReactCurrentOwner.current,
props,
);
}
我们分析上面的代码过程
1. 拦截 ref 和 key
if (hasValidRef(config)) {
ref = config.ref;
}
if (hasValidKey(config)) {
key = '' + config.key;
}
如果ref
或key
符合要求,那么将两者单独取出来,而不会放到下面的props
,这也验证了我们在父组件传给子组件ref
或key
的 prop 时,在子组件是取不到的,因为一开始就被拦截掉了
2. 处理除了 ref、key、__self 和__source 的 props
for (propName in config) {
if (
/**
* hasOwnProperty.call(config, propName)的意思就是只取传给组件的prop,而不取继承而来的
* RESERVED_PROPS即包含key、ref、__self、__source,后两个只用于开发环境,可以忽略
*/
hasOwnProperty.call(config, propName) &&
!RESERVED_PROPS.hasOwnProperty(propName)
) {
props[propName] = config[propName];
}
}
3. 处理 children
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;
}
我们看createElement(type,config,children)
接收了三个参数,但实际上一个父节点可以有多个子节点,如下面的div
有两个子节点
<div className=".app">
<span>span1</span>
<span>span2</span>
</div>
那么Babel
会转化为如下代码
React.createElement(
'div', // type
{ className: ".app" }, // config
{
$$typeof: Symbol(react.element),
key: null,
props: {children: 'span1'},
ref: null,
type: "span",
},
{
$$typeof: Symbol(react.element),
key: null,
props: {children: 'span2'},
ref: null,
type: "span"
}
);
也就是说,如果有多个 child,那么会在第二个参数后按顺序传入每个 child(这里会先用createElement
处理子节点,然后再处理父节点,所以我们可以看到传入的 child 已经处理好了)
所以下面的代码才用arguments
的长度减 2 来获取真正传入 child 的数量
const childrenLength = arguments.length - 2;
当然这里可以用 es6 的...children
来替代
之后判断 child 数量,如果只有一个,直接就赋值给props.children
,否则将所有的child
放入数组,再放到props.children
上
if (childrenLength === 1) {
// child数量只有一个,直接就赋值给props.children
props.children = children;
} else if (childrenLength > 1) {
// 否则将所有的`child`放入数组
const childArray = Array(childrenLength);
for (let i = 0; i < childrenLength; i++) {
childArray[i] = arguments[i + 2];
}
props.children = childArray;
}
4. 处理 defaultProps
如果有传入了defaultProps
,比如
MyComponent.defaultProps = { prop1: 'x', prop2: 'xx' ...}
如果没传给组件prop
,或者传了但值为undefined
,那么判断到有传defaultProps
,会去取defaultProps
上对应的prop
if (type && type.defaultProps) {
const defaultProps = type.defaultProps;
for (propName in defaultProps) {
if (props[propName] === undefined) {
props[propName] = defaultProps[propName];
}
}
}
我们看到上面是用 ===
来判断的,则如果我们给组件传prop1
为 null,那么组件里面得到的会是 null,而不是defaultProps
上的prop1: 'x'
5. 最后调用 ReactElement
ReactElement 代码不多,就是将传入参数组合成一个element
对象并返回
const ReactElement = function(type, key, ref, self, source, owner, props) {
// self, source用于DEV,这里略过
const element = {
$$typeof: REACT_ELEMENT_TYPE,
type: type,
key: key,
ref: ref,
props: props,
};
return element;
};
$$typeof: REACT_ELEMENT_TYPE
代表 ReactElement 类型,实质是一个Symbol
值,为Symbol.for('react.element')
type
即为组件类型,对于原生 dom,type
则是对应的tag
,如div
,span
等,对于函数组件FnComp
、类组件ClassComp
则是对应的函数或类,如ƒ FnComp()
,class ClassA
import React
为何为高亮
这是由于在(j|t)sconfig.json
里面默认有个配置:
如果你改为自己写的如Feact.createElement
,如"jsxFactory": "Feact.createElement"
,那么我们可以看到原先的import React
就不会高亮了
不过以上只是在编辑器层面上的显示作用,要真正起作用还是得配置Babel
jsx-runtime
React17
提供一个全新版本的 JSX 转换,即不用再显式import React
,原因是React
与Babel
合作推出了 @babel/plugin-transform-react-jsx,该plugin
会默认到react
目录下的 jsx-runtime.js 文件读取相应的jsx
、jsxs
等函数来转译相应的JSX
jsx 函数基本与createElement
相同,但有一点要注意,在createElement
里面要处理children
,而在jsx
里面,会直接在config.children
里面得到children
,省去了处理环节
function App() {
return <div className=".app">
<span>span1</span>
<span>span2</span>
</div>
}
自定义你自己的 jsx-runtime
比如你之后觉得自己行了,想写一个react-like
库,比如feact
,那你可以在 feact/jsx-runtime.js 下提供对应的jsx, jsxs
等
然后必须在 webpack.config.js 里面给@babel/plugin-transform-react-jsx
加上importSource
为feact
plugins: [
[require.resolve('@babel/plugin-transform-flow-strip-types')],
['@babel/plugin-transform-react-jsx',{runtime:'automatic', importSource: 'feact'}]
]
总结
- 😯,原来在 React17 之前必须显式
import React
是因为Babel
会默认将JSX
通过React.createElement
转译,如果不引入React
,那自然就取不到createElement
,也就自然会报错了 - 😯,原来
JSX
会被Babel
转译为如下对象
{
$$typeof: Symbol(react.element),
key: null,
props: {children: 'span1'},
ref: null type: "span",
}
- 😯,原来
Babel
会先转译子节点,再转译父节点 - 😯,原来 React17 不用
import React
是因为通过@babel/plugin-transform-react-jsx
去自动引入react/jsx-runtime.js
里面的jsx、jsxs
等 - 😯,原来要自定义
jsx-runtime
,可以加上配置importSource: 'YourPackage'
,并在YourPackage/jsx-runtime
下 export 出jsx、jsxs
等
6) 😯,原来在 createElement
里面要处理 children
,而在 jsx
里面,会直接在 config.children
里面得到 children
最后
感谢留下足迹,如果您觉得文章不错 😄😄,还请动动手指 😋😋,点赞+收藏+转发 🌹🌹
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。