【React】JSX底层渲染机制

前言

  • 细阅此文章大概需要

    7分钟\color{red}{7分钟}

    左右

  • 本篇中讲述了:

    1. 从编写JSX到页面渲染出来都发生了什么?
    2. 创建虚拟DOM
    3. 渲染真实DOM
  • 欢迎在评论区探讨、留言,如果认为有任何错误都还请您不吝赐教,万分感谢。希望今后能和大家共同学习、进步。

  • 下一篇会尽快更新,已经写好的文章也会在今后进行不定期的修订、更新。

  • 如果觉得这篇文章对您有帮助,还请点个赞支持一下,谢谢大家!

  • 欢迎转载,注明出处即可。


从编写JSX到页面渲染出来都发生了什么?

  1. 将我们编写的JSX语法,编译成虚拟DOM对象(virtualDOM)

    虚拟DOM对象(virtualDOM):框架自己内部自己构建的一套对象体系【对象的相关成员都是react内部规定的】,基于这些属性描述出,我们所构建视图中的DOM节点相关的特征

  2. 把构建的虚拟DOM渲染为真实DOM

    真实DOM:浏览器页面中,最后渲染出来用户能够看到的DOM元素

  3. 首次渲染是直接将虚拟DOM对象渲染为真实DOM,但后期视图更新时,需要经过DOM-Diff的对比,计算出“补丁包(patch)”,实际上就是两次要渲染视图的差异部分,然后将补丁包进行差异渲染

    首次渲染:JSX语法->虚拟DOM对象->真实DOM

    差异渲染:按照最新的数据将JSX语法全部重新编译为->新的虚拟DOM对象->与旧的虚拟DOM对象DOM-DIFF生成patch(补丁包)->将Patch部分渲染为真实DOM(只重新渲染补丁包)

知道了大体流程,我们来看一看react具体是怎么来做的这些事。


JSX底层渲染机制【创建虚拟DOM】

1. JSX是如何编译为虚拟DOM的?

  1. 首先webpack打包时基于babel-preset-react-appjsx代码编译为React.createElement...这种形式

    1. React.createElement(ele, props, ...children) 的参数 【只要是元素节点,必然会基于React.createElement进行处理】
    • ele:【节点类型】元素标签名或组件 如(react.fragment、’div’、’h2’等)
    • props:【元素的属性集合(对象)】如果没有设置过任何属性,则此时为null 如(null、{classname:’aaa’, style:…..}等)
    • children:第三个及以后的参数都是当前元素的子节点 如(子节点:React.createElement…、’字符串’等)
  2. 再将编译后生成的这些React.createElement(...)方法执行,创建出虚拟DOM对象(也称jsx元素jsx对象…)

       // React.createElement(...)方法执行后创建出的虚拟DOM对象
       virtualDOM={
        $$typeof【固定值】: symbol(React.element),
        ref:null,
        key:null,
        type:标签名【或组件】,
        // 存储了元素的相关属性&&子节点信息
        props【肯定是一个对象,哪怕是空的】:{
            ...元素相关属性,
            children【子节点信息,如果没有子节点就没有这个属性。】:可能是一个值,也可能是一个数组。
       }
    
  3. 实现一个简单的createElement方法

    我们可以模拟react的做法来实现一个简单的createElement方法,来看一下虚拟DOM对象的核心创建流程。

      export function createElement(ele, props, ...children){
        // 声明一个初始的虚拟dom对象
        let virtualDOM={
          $$typeof: symbol(React.element),
          ref:null,
          key:null,
          type:null,
          props:{}
        } 
        let len = children.length
        virtualDOM.type = ele
        // 判读props属性并赋值
        if(props!==null){
          virtualDOM.props = {
            ...props
          }
        }
        // 判断子元素是否长度,并赋值
        if(len===1) virtualDOM.props.children = children[0]
        if(len>0) virtualDOM.props.children = children
        return virtualDOM
      }
    
  4. 其他

    胡子语法中不能直接嵌入除数组外的其他对象,但是有一个对象是可以直接嵌入的,那就是虚拟dom对象【JSX元素对象】

    
        <div>
            {OBJ} // ERROR
        </div>
        
        <div>
            {[<COMPONENTS/>,....]} // 被支持的写法
        </div>
        
        <div>
            {{ $$typeof: symbol(React.element), ref:null, key:null, type:null, props:{...children} }} // 被支持的写法
        </div>
    

    同理我们也可以基于createElement语法来构建视图【写起来太麻烦,基本不会这么搞,但是可行】

     <div>
       {React.createElement(ele, props, ...children)} // 被支持的写法
     </div>
    

    jsx语法实际上也是会编译成createElement语法来生成虚拟dom对象


JSX底层渲染机制【渲染真实DOM】

1. React是如何把虚拟DOM渲染成真实DOM的

不管是React16版本还是React18版本都是基于ReactDOM中的render方法处理的,只不过稍有区别:

v16:

// v16
    ReactDOM.render(
        <>.....</>,
        document.getElementById('root')
    )

v18:

// v18
    const root = ReactDOM.createRoot(document.getElementById('root'))
    root.render(
      <>.....</>
    )

接下来从render方法来看看react到底是怎么将虚拟DOM渲染为真实DOM的

在上一节当中,我们知道虚拟DOM对象就是React框架内部自己构建的一套对象体系,作用就是将JSX编译为虚拟DOM对象后,能够基于这些对象当中所存储的属性描述出我们所构建视图中DOM节点的相关特征。

而React当中的render方法,就是结合虚拟对象中所描述的这些信息,基于JS当中操作DOM的那些API来创建、编排DOM节点,最终将虚拟DOM渲染为真实DOM。

如:

  • document.createElement
  • [ELEMENT].appendChild
  • [ELEMENT].setAttribute

封装一个遍历对象私有属性的方法

首先在虚拟DOM里对于节点的描述中,肯定会有一个属性props,其属性值是该节点的属性集合(对象),当我们想要将这个虚拟DOM渲染为真实DOM时,肯定需要给这个DOM节点添加相应的属性。那我们可以先封装一个遍历对象私有属性的方法,便于我们后续遍历Props的这个操作。

基于传统的for/in循环,会存在一些弊端:

  • 性能较差(既可以迭代私有的,也可以迭代公有的)
  • 只能迭代“可枚举、非Symbol类型的”属性

于是我们需要自己来封装一个遍历对象私有属性的方法

首先我们要做的事是:获取对象所有的私有属性【私有的、不论是否可枚举、不论类型】

  • Object.getOwnPropertyNames(arr):获取对象非Symbol类型的私有属性(无关是否可枚举)
  • Object,getOwnPropertySymbols(arr):获取Symbol类型的私有属性

结合上面的两个方法我们可以获取对象的所有的私有属性名:

// 获取对象的所有的私有属性名
let keys = object.getownPropertyNames(arr).concat(object.getownPropertySymbols(arr));

而在ES6当中我们可以通过Reflect.ownKeys代替上述操作(弊端:不兼容IE)

// 获取对象的所有的私有属性名
let keys = Reflect.ownKeys(arr);

完整方法:

  // 遍历对象私有属性的方法
  const each = function each(obj, callback){
    if (obj === null || typeof obj !== "object") throw new TypeError('obj is not a object')
    if (typeof callback !== "function") throw new TypeError('callback is not a function')
    let keys = Reflect.ownKeys(obj)
    keys.forEach(key => {
        let value = obj[key]
        callback(value, key) // 每次循环都将回调执行
    }
  }

2. 实现一个简易的render方法

接下来通过一个简易的render来了解一下,React当中的render核心都做了哪些事:

/*render:把虚拟DOM渲染为真实DOM*/
export function render(virtualDOM,container){ // 方法接收两个参数,一个是虚拟DOM对象,一个是挂载的容器
    let { type, props } = virtualDOM // 先从虚拟DOM对象中解构出节点类型和节点的相关属性
    if (typeof type === "string"){ //类型是字符串,存储的是标签名:动态创建这样一个标签
        let ele = document.createElement(type)
        each(props,(value,key)=>{ // 刚才封装的遍历对象私有属性的方法,为标签设置相关的属性&子节点
            //【className的处理】:value存储的是样式类名
            if (key ==='className'){
                ele.className = value
                return
            }
            //【style的处理】:value:存储的是样式对象
            if (key ==='style'){
                each(value,(val, attr)=>{ // 遍历styles,为节点设置样式
                    ele.style = val
                })
                return
            }
            //【子节点的处理】:value存储的children属性值
            if (key ==='children'){
                let children = value
                if(!Array.isArray(children)) children = [children] // 处理只有一个子节点的情况
                children.forEach(child=>{
                    // 判断子节点类型:文本节点直接插入即可
                    if(/^(string|number)$/.test(typeof child)){ // 文本和数字都属于文本节点
                        ele.appendChild(document.createTextNode(child))
                        return
                    }
                    // 判断子节点类型:子节点依然是一个虚拟DOM【递归处理】
                    render(child, ele)
                })
                return
            }
            // 其他属性先简写
            ele.setAttribute(key,value);
         });
        // 把新增的标签,增加到指定容器中
        centainer.appendChild(ele);
     }

测试通过我们封装的方法来生成虚拟DOM并渲染真实DOM

    import React from 'react'
    import ReactDOM from 'react-dom/client'
    import { createElement, render } from './myJsxHandle.js'
    
    const root = ReactDOM.createRoot(document.getElementById('root')) //根节点
    
    let styles = {//...样式}
    let x = 10
    let y = 20
    
    // JSX形式
    // root.render(
    //    <div className='container'>
    //        <h1 className='title' style={styles}>Title</h1>
    //         <div className='box'>
    //            <span>{x}</span>
    //            <span>{y}</span>
    //        </div>
    //    </div>
    // ) 
    
    // 基于我们封装的createElement语法来构建视图
    let myVirtualDOM = createElement(
        'div',
        {className: 'container'},
        createElement(
            'h1',
            {className: 'title', style: styles},
            'Title'
        ),
        createElement(
            'div',
            {className: 'box'},
            createElement('span', null, x),
            createElement('span', null, y)
        )
    )
    
    // 基于我们封装的render,使用虚拟DOM渲染真实DOM
    render(myVirtualDOM, document.getElementById('root'))
    
    
    

其他

在生成真实DOM的过程中,需要给元素设置属性。通常设置属性的方式有两种,我们选择通过内置api设置的方式

为元素设置属性有两种方式:【自定义、内置】,使用时需要注意他们的区别

  1. 元素.属性 = 属性
  • 自定义属性的方式实际上是给对象的堆内存空间中新增成员,不会设置到元素标签上

  • 获取:元素.属性

  • 删除: delete 元素.属性

  1. 元素.setAttribute(属性,属性值)
  • 内置属性,是直接写在元素的标签上
  • 获取: getAttribute
  • 删除: removeAttribute

【React】JSX底层渲染机制

原文链接:https://juejin.cn/post/7341394494900453413 作者:Hypnotize

(0)
上一篇 2024年3月3日 上午10:07
下一篇 2024年3月3日 上午10:17

相关推荐

发表评论

登录后才能评论