React 之 JSX

一、我们写的 JSX 终将变成什么

万物始于 jsx,想要深入学习 react ,就应该从 jsx 入手。弄清楚 jsx ,方便学习掌握以下内容:

  • 了解常用的元素会被 React 处理成什么,有利于后续理解 react fiber 类型;
  • 理解 jsx 的编译过程,方便操纵 children、控制 React 渲染,有利于便捷使用 React 插槽组件。

我写了一段 react 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>
    }
}

在这里插入图片描述

1 babel 处理后的样子

首先,看一下上述例子中的 jsx 模版会被babel编译成什么?

在这里插入图片描述

和如上看到的一样,我写的 JSX 元素节点会被编译成 React Element 形式。那么,我们首先来看一下 React.createElement 的用法。

React.createElement(
  type,
  [props],
  [...children]
)

createElement 参数:

  • 第一个参数:如果是组件类型,会传入组件对应的类或函数;如果是 dom 元素类型,传入 div 或者 span 之类的字符串。
  • 第二个参数:一个对象,在 dom 类型中为标签属性,在组件类型中为 props 。
  • 其他参数:依次为 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!"
    )

|--------问与答--------|

问:老版本的 React 中,为什么写 jsx 的文件要默认引入 React? 如下

import React from 'react'
function Index(){
    return <div>hello,world</div>
}

答:因为 jsx 在被 babel 编译后,写的 jsx 会变成上述 React.createElement 形式,所以需要引入 React,防止找不到 React 引起报错。

|---------end---------|

2 createElement 处理后的样子

然后点击按钮,看一下写的 demo 会被 React.createElement 变成什么:

在这里插入图片描述

从上面写的 jsx 结构来看,外层的 div 被 react.createElement 转换成 react element 对象,div 里面的 8 个元素分别转换成 children 子元素列表。下面就是 jsx 的转换规则,请一定要记住,以便后续能更流畅地使用 jsx 语法。

jsx元素类型react.createElement 转换后type 属性
element元素类型react element类型标签字符串,例如 div
fragment类型react element类型symbol react.fragment类型
文本类型直接字符串
数组类型返回数组结构,里面元素被react.createElement转换
组件类型react element类型组件类或者组件函数本身
三元运算 / 表达式先执行三元运算,然后按照上述规则处理看三元运算返回结果
函数执行先执行函数,然后按照上述规则处理看函数执行返回结果

3 React 底层调和处理后,终将变成什么?

最终,在调和阶段,上述 React element 对象的每一个子节点都会形成一个与之对应的 fiber 对象,然后通过 sibling、return、child 将每一个 fiber 对象联系起来。

所以,我们有必要先来看一下 React 常用的 fiber 类型,以及 element 对象和 fiber 类型的对应关系。

不同种类的 fiber Tag

React 针对不同 React element 对象会产生不同 tag (种类) 的fiber 对象。首先,来看一下 tag 与 element 的对应关系:

export const FunctionComponent = 0;       // 函数组件
export const ClassComponent = 1;          // 类组件
export const IndeterminateComponent = 2;  // 初始化的时候不知道是函数组件还是类组件 
export const HostRoot = 3;                // Root Fiber 可以理解为根元素 , 通过reactDom.render()产生的根元素
export const HostPortal = 4;              // 对应  ReactDOM.createPortal 产生的 Portal 
export const HostComponent = 5;           // dom 元素 比如 <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 结构图

最终写的 jsx 会变成如下格式:

在这里插入图片描述

fiber 对应关系

  • child: 一个由父级 fiber 指向子级 fiber 的指针。
  • return:一个子级 fiber 指向父级 fiber 的指针。
  • sibling: 一个 fiber 指向下一个兄弟 fiber 的指针。

温馨提示:

  • 对于上述在 jsx 中写的 map 数组结构的子节点,外层会被加上 fragment ;
  • map 返回数组结构,作为 fragment 的子节点。

二、进阶实践-可控性 render

上面的 demo 暴露出了如下问题:

  1. 返回的 children 虽然是一个数组,但是数组里面的数据类型却是不确定的,有对象类型( 如ReactElement ) ,有数组类型(如 map 遍历返回的子节点),还有字符串类型(如文本);
  2. 无法对 render 后的 React element 元素进行可控性操作。

针对上述问题,我们需要对demo项目进行改造处理,具体过程可以分为4步:

  1. 将上述children扁平化处理,将数组类型的子节点打开 ;

  2. 干掉children中文本类型节点;

  3. 向children最后插入say goodbye元素;

  4. 克隆新的元素节点并渲染。

希望通过这个实践 demo ,大家可以加深对 jsx 编译后结构的认识,学会对 jsx 编译后的 React.element 进行一系列操作,达到理想化的目的,以及熟悉 React API 的使用。

由于,我们想要把 render 过程变成可控的,因此需要把上述代码进行改造。

class Index extends React.Component{
    status = false /* 状态 */
    renderFoot=()=> <div> i am foot</div>
    /* 控制渲染 */
    controlRender=()=>{
        const reactElement = (
            <div style={{ marginTop:'100px' }} className="container"  >   
                 { /* 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>
        )
        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(flatChildren,(item)=>{
            if(React.isValidElement(item)) newChildren.push(item)
        })
        /* 第3步,插入新的节点 */
        const lastChildren = React.createElement(`div`,{ className :'last' } ,`say goodbye`)
        newChildren.push(lastChildren)
        
        /* 第4步:修改容器节点 */
        const newReactElement =  React.cloneElement(reactElement,{} ,...newChildren )
        return newReactElement
    }
    render(){
        return this.controlRender()
    }
}

第 1 步:React.Children.toArray 扁平化,规范化 children 数组。

const flatChildren = React.Children.toArray(children)
console.log(flatChildren)

React.Children.toArray 可以扁平化、规范化 React.element 的 children 组成的数组,只要 children 中的数组元素被打开,对遍历 children 很有帮助,而且 React.Children.toArray 还可以深层次 flat 。

打印结果:

第 2 步:遍历 children ,验证 React.element 元素节点,除去文本节点。

const newChildren :any= []
React.Children.forEach(flatChildren,(item)=>{
    if(React.isValidElement(item)) newChildren.push(item)
})

用 React.Children.forEach 去遍历子节点,如果是 react Element 元素,就添加到新的 children 数组中,通过这种方式过滤掉非 React element 节点。React.isValidElement 这个方法可以用来检测是否为 React element 元素,接收一个参数——待验证对象,如果是返回 true , 否则返回 false 。

这里可能会有一个疑问就是如下:

难道用数组本身方法 filter 过滤不行么 ? 为什么要用 React.Children.forEach 遍历?

这种情况下,是完全可以用数组方法过滤的,因为 React.Children.toArray 已经处理了 children ,使它变成了正常的数组结构 也就是说 React.Children.forEach = React.Children.toArray + Array.prototype.forEach

React.Children.forEach 本身就可以把 children 扁平化了,也就是上述第一步操作多此一举了。为什么要有第一步,主要是更多的学习一下 React api。

第 3 步:用 React.createElement ,插入到 children 最后

 /* 第三步,插入新的节点 */
const lastChildren = React.createElement(`div`,{ className :'last' } ,`say goodbye`)
newChildren.push(lastChildren)

上述代码实际等于用 JSX 这么写:

newChildren.push(<div className="last" >say goodbye</div>)

第 4 步: 已经修改了 children,现在做的是,通过 cloneElement 创建新的容器元素。

为什么要用 React.cloneElement ,createElement 把上面写的 jsx,变成 element 对象; 而 cloneElement 的作用是以 element 元素为样板克隆并返回新的 React element 元素。返回元素的 props 是将新的 props 与原始元素的 props 浅层合并后的结果。

这里 React.cloneElement 做的事情就是,把 reactElement 复制一份,再用新的 children 属性,从而达到改变 render 结果的目的。

/* 第 4 步:修改容器节点 */
const newReactElement =  React.cloneElement(reactElement,{} ,...newChildren )

效果

在这里插入图片描述

验证 :

  • ① children 已经被扁平化。
  • ② 文本节点 my name is alien 已经被删除。
  • <div className="last" > say goodbye</div> 元素成功插入。

达到了预期效果。

|--------问与答--------|

问: React.createElement 和 React.cloneElement 到底有什么区别呢?

答: 可以完全理解为,一个是用来创建 element 。另一个是用来修改 element,并返回一个新的 React.element 对象。

|---------end---------|

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值