认识 JSX
目录
const toLearn = [ 'react' , 'vue' , 'webpack' , 'nodejs' ]
const TextComponent = ()=> <div> hello , i am function component </div>
class Index extends React.Component{
status = false /* 状态 */
renderFoot=()=> <div> i am foot</div>
render(){
/* 以下都是常用的jsx元素节 */
return <div style={{ marginTop:'100px' }} >
{ /* element 元素类型 */ }
<div>hello,world</div>
{ /* fragment 类型 */ }
<React.Fragment>
<div> 👽👽 </div>
</React.Fragment>
{ /* text 文本类型 */ }
my name is alien
{ /* 数组节点类型 */ }
{ toLearn.map(item=> <div key={item} >let us learn { item } </div> ) }
{ /* 组件类型 */ }
<TextComponent/>
{ /* 三元运算 */ }
{ this.status ? <TextComponent /> : <div>三元运算</div> }
{ /* 函数执行 */ }
{ this.renderFoot() }
<button onClick={ ()=> console.log( this.render() ) } >打印render后的内容</button>
</div>
}
}
JSX 终将变成什么
JSX 经 babel 处理后
JSX 元素节点会被编译成 React Element 形式
React.createElement(
type,
[prop],
[...children]
)
createElement 参数:
- 第一个参数
- 组件类型:传入组件对应的 类 或 函数
- DOM 元素类型:传入代表元素的字符串,诸如 div、span
- 第二个参数(对象)
- 组件类型:props
- DOM 元素类型:标签属性
- 其他参数
- 依次为 children,根据顺序排列
举个例子
<div>
<TextComponent />
<div>hello, world</div>
let us learn React!
</div>
// 上述代码会被 babel 编译为
React.createElement("div", null,
React.createElement(TextComponent, null),
React.createElement("div", null, "hello world"),
"let us learn React!"
)
JSX 的转换规则
| JSX 元素 | React.createElement 转换后 | type 属性 |
|---|---|---|
| 组件类型 | react element 类型 | 组件类 或者 组件函数本身 |
| element 元素类型 | react element 类型 | 标签字符串,例如 div、span |
| fragment 类型 | react element 类型 | symbol react.fragment 类型 |
| 文本类型 | 字符串 | 无 |
| 数组类型 | 返回数组结构,数组元素被 React.createElement 转换 | 无 |
| 三元运算 / 表达式 | 先执行三元运算,然后按照上述规则处理 | 由三元运算返回结果所定 |
| 函数执行 | 先执行函数,然后按照上述规则处理 | 由函数执行返回结果所定 |
React 底层调好处理后,上述 react element 终将变成什么
最终,在调和阶段,上述 react element 对象的每一个子节点都会形成一个与之对应的 fiber 对象,然后通过 return、sibling、child 将每个 fiber 对象联系起来。
那么 React 常用的 fiber 类型,以及 element 对象和 fiber 类型的对应关系是怎样的?
不同类型的 fiber tag
export const FunctionComponent = 0; // 函数组件
export const ClassComponent = 1; // 类组件
export const IndeterminateComponent = 2; // 初始化时不知道是函数组件或是类组件
export const HostRoot = 3; // Root Fiber 可以理解为根元素,通过 reactDom.render() 产生的根元素
export const HostProtal = 4; // 对应 ReactDOM.createPortal 产生的 Portal
export const HostComponent = 5; // element 元素 比如 <div>
export const HostText = 6; // 文本节点
export const Fragment = 7; // 对应 <React.Fragment>
export const Mode = 8; // 对应 <React.StrictMode>
export const ContextConsumer = 9; // 对应 <Context.Consumer>
export const ContextProvider = 10; // 对应 <Context.Provider>
export const ForwardRef = 11; // 对应 React.ForwardRef
export const Profiler = 12; // 对应 <Profiler />
export const SuspenseComponent = 13; // 对应 <Suspense>
export const MemoComponent = 14; // 对应 React.memo 返回的组件
JSX 最终形成的 fiber 结构图
fiber 对应关系
- child:一个由父级 fiber 指向子级 fiber 的指针
- return:一个由子级 fiber 指向父级 fiber 的指针
- sibling:一个由兄弟 fiber 指向下一个兄弟 fiber 的指针
tips:
- 对于上述在 JSX 中 map 数组结构的子节点,最外层会被加上 fragment
- map 返回的数组元素,会作为 fragment 的子节点
进阶实践-可控性 render
上述 demo 暴露出如下问题:
- render() 返回的 children 虽然是一个数组,但是数组元素的数据类型不确定
- 对象类型,如 react element
- 数组类型,如 map 遍历后返回的子节点
- 字符串类型,如 文本
- 无法对 render 后的 react element 元素进行可控性操作
针对上述问题,需要对 demo 代码进行改造处理,具体过程可以分为 4 步:
- 将 render() 返回的 chilren 做扁平化处理,将数组类型的子节点打开;
- 去除 children 中的文本类型子节点
- 在 children 最后插入 say goodbye 元素
- 克隆新的元素节点并渲染
class Index extends React.Component {
status = fasle /* 状态 */
renderFoot = () => <div> i am foot </div>
/* 控制渲染 */
controlRender = () => {
const reactElement = (
<div style={{ marginTop: "100px" }} className="container">
{ /* element 元素类型 */ }
<div>hello, world</div>
{ /* fragment 类型 */ }
<React.Fragment>
<div>huaqi</div>
</React.Fragment>
{ /* text 文本类型 */ }
my name is huaqi
{ /* 数组节点类型 */ }
{ toLearn.map(item => <div key={item} >let use learn { item } </div>) }
{ /* 组件类型 */ }
<TextComponent />
{ /* 三元运算 */ }
{ this.status ? <TextComponent /> : <div>三元运算</div> }
{ /* 函数执行 */ }
{ this.renderFoot() }
<button onClick={ () => console.log( this.render() ) }>打印 render 后的内容</button>
</div>
)
console.log(reactElement)
const { children } = reactElement.props
/* 1. 扁平化 children */
const flatChildren = React.Children.toArray(children)
console.log(flatChildren)
/* 2. 去除文本节点 */
const newChildren: any = []
React.Children.forEach(flathChildren, item => {
if (React.isValidElement(item)) {
newChildren.push(item)
}
})
/* 3. 插入新节点 */
const lastChild = React.createElement("div", { className: "last" }, "say goodbye")
newChildren.push(lastChild)
/* 4. 修改容器节点 */
const newReactElement = React.cloneElement(reactElement, {}, ...newChilren)
return newReactElement
}
render() {
return this.controlRender()
}
}
React.Children.toArray() 扁平化、规范化 children 数组
React.Children.toArray() 可以扁平化、规范化 React.element 的 children 组成的数组。
children 中的数组元素打开后,对遍历 children 很有帮助。
React.Children.toArray() 可以深层次 flat。
遍历 children,验证 React.element 元素节点,去除文本节点
使用 React.Children.forEach 遍历子节点,如果是 react element 元素,就添加到到新的 children 数组中,通过这种方式过滤掉非 react element 节点。
Rreact.isValidElement() 可以用于检测参数是否为 react element 元素。
tips
React.Children.forEach() = React.Children.toArray() + Array.prototype.forEach()
React.Children.forEach() 本身可以将 children 扁平化。
使用 React.createElement(),创建 react element 并将其插入到 children 最后
const lastChildren = React.createElement("div", { className: "last" }, "say goodbye")
newChildren.push(lastChildren)
// 上述代码使用 JSX 如下
newChildren.push(<div className="last">say goodbye</div>)
使用 React.cloneElement() 创建新的容器元素
- React.createElement():将 JSX 代码转化为 element 对象
- React.cloneElement(): 以 element 元素为样板克隆并返回新的 react element 元素
- 返回元素的 props 是将新的 props 与原始元素的 props 浅层合并的结果
Babel 解析 JSX 流程
@babel/plugin-syntax-jsx 和 @babel/plugin-transform-react-jsx
JSX 语法编译实现来源于两个插件
- @babel/plugin-syntax-jsx:使用这个插件,能够让 Babel 有效的解析 JSX 语法。
- @babel/plugin-transform-react-jsx:这个插件内部调用了 @babel/plugin-syntax-jsx,可以将 React JSX 转换为 JS 能够识别的 createElement 格式。
Automatic Runtime
新版本 React 已经不需要引入 crateElement,这种模式来源于 Automatic Runtime。
业务代码中的 JSX 文件:
function Index() {
return <div>
<h1>hello, world</h1>
<span>let us learn React</span>
</div>
}
被编译后的文件:
import { jsx as _jsx } from "react/jsx-runtime"
import { jsxs as _jsxs } from "react/jsx-runtime"
function Index() {
return _jsxs("div", {
children: [
_jsx("h1", {
children: "hello, world"
}),
_jsx("span", {
children: "let us learn React",
}),
],
});
}
可见,@babel/plugin-syntax-jsx 已经向文件中提前注入了 _jsxRuntime api。不过这种模式需要在 .babelrc 设置 runtime:automatic
"presets": [
["@babel/preset-react", {
"runtime": "automatic"
}]
]
Classic Runtime
经典模式下,使用 JSX 的文件需要手动引入 React,否则会报错。
业务代码的 JSX 文件:
import React from "react"
function Index() {
return <div>
<h1>hello, world</h1>
<span>let us learn React</span>
</div>
}
被编译后文件:
import React from "react"
function Index() {
return React.createElement(
"div",
null,
React.createElement("h1", null, "hello world"),
React.createElement("span", null, "let us learn React")
);
}
Q & A
老版本的 React 中,为什么写 JSX 的文件要默认引入 React?
如下
import React from "react"
function Index() {
return <div>hello, world</div>
}
答:因为 JSX 被 babel 编译后,JSX 代码会变成上述 React.createElement 形式,所以需要引入 React,防止找不到 React 引起报错。