自学内容网 自学内容网

【React】JSX底层处理机制


React 在内部会将 JSX 转换为 JavaScript 代码,通过 React.createElement 转换成 virtualDOM ,最后通过 render渲染成真实DOM。

1、JSX语法转virtualDOM

虚拟DOM对象:框架自己内部构建的一套对象体系(对象的相关成员都是React内部规定的),基于这些属性描述出,我们所构建视图中的,DOM节点的相关特征
(1)基于babel-preset-react-app把JSX编译为React.createElement(…)这种格式,只要是元素节点,必然会基于createElement处理,可以通过 https://babeljs.io/repl 输入JSX代码,可以实时看到编译之后的结果

React.createElement(ele,props,...children)
  • ele:元素标签名(或者组件)
  • props:元素属性集合(对象),如果没有设置过任何的属性,则此值是null
  • children:第三个及以后的参数,都是当前元素的子节点
//JSX
<>
   <div className='container'>
      <h2 className="title" style={styObj}>hello world</h2>
      <div className="box">
        <span>{x}</span>
        <span>{y}</span>
      </div>
    </div>
</>
 React.createElement(
  React.Fragment,
  null,
     React.createElement(
    "div",
    {
      className: "container"
    },
      React.createElement(
      "h2",
      {
        className: "title",
        style: styObj
      },
      "hello world"
    ),
     React.createElement(
      "div",
      {
        className: "box"
      },
      React.createElement("span", null, x),
      React.createElement("span", null, y)
    )
  )
);

在这里插入图片描述

(2)再把createElement方法执行,创建出virtualDOM「也有称之为:JSX元素、JSX对象、ReactChild对象…」

    virtualDOM = {
        $$typeof: Symbol(react.element),
        ref: null,
        key: null,
        type: 标签名「或组件」,
        // 存储了元素的相关属性 && 子节点信息
        props: {
            元素的相关属性,
            children:子节点信息「没有子节点则没有这个属性、属性值可能是一个值、也可能是一个数组」
        }
      }

这里我们打印下createElement执行之后得到的virtualDOM!

let jsxObj =  React.createElement(
  React.Fragment,
  null,
  React.createElement(
    "div",
    {
      className: "container"
    },
      React.createElement(
       "h2",
       {
        className: "title",
        style: styObj
      },
       "hello world"
    ),
     React.createElement(
      "div",
      {
        className: "box"
      },
      React.createElement("span", null, x),
      React.createElement("span", null, y)
    )
  )
);
console.log(jsxObj,'jsxObj')

在这里插入图片描述

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

真实DOM:浏览器页面中,最好渲染出来的可以被看到的DOM元素。
渲染为真实DOM是基于ReactDOM中的render方法处理

//react16版本
ReactDOM.render(<>.....</>,document.getElementId('root))
const root = ReactDOM.createRoot(document.getElementId('root))
root.render(<>xxxxxxxx</>)

补充说明:

第一次渲染页面是直接从virtualDOM->真实DOM;
后期视图更新的时候,需要经过一个DOM-DIFF的对比,计算出补丁包PATCH(两次视图差异的部分),把PATCH补丁包进行渲染

3、手写createElement

createElement:创建virtualDOM对象。首先virtualDOM对象的结构如下:

let virtualDOM = {
   $$typeof:Symbol("react.element"),
   key:null,
   ref:null,
   type:null,
   props:{}//标签上属性以及子节点的属性
}

在src目录下创建jsxHandle.js文件,导出一个名为createElement的函数。children 的类型是一个数组(由于使用了 …children 语法)。它可能会包含字符串、数字、React 元素或者是嵌套的数组。

使用createElement,如下:

React.createElement(
  React.Fragment,
  null,
  React.createElement(
    "div",
    {
      className: "container"
    },
      React.createElement(
       "h2",
       {
        className: "title",
        style: styObj
      },
       "hello world"
    ),
     React.createElement(
      "div",
      {
        className: "box"
      },
      React.createElement("span", null, x),
      React.createElement("span", null, y)
    )
  )
);

手写createElement,如下:

  • 声明virtualDOM需要的属性
  • 设置virtualDOM的typeele
  • 设置virtualDOM的propsprops
  • 设置virtualDOM的children为传过来的children(需要特殊处理)
export function createElement(ele, props, ...children) {
    let virtualDOM = {
        $$typeof: Symbol('react.element'),
        key: null,
        ref: null,
        type: null,
        props: {}
    };
    let len = children.length;
    virtualDOM.type = ele;
    if(props!==null){
        virtualDOM.props = {
            ...props
       }
    };
    console.log(len,'len',children)
    if(len==1) virtualDOM.children = children[0];
    if(len >1) virtualDOM.children = children;
    return  virtualDOM
};

打印出 console.log(len,'len',children),可见children类型为Array

在这里插入图片描述

解释下上面的一些细节,使用下面JSX代码进行说明:

  • <></>下只有div一个子元素,所以在虚拟DOM第一层的propschildrenlength为1
  <>
   <div className='container'>
      <h2 className="title" style={styObj}>hello world</h2>
      <div className="box">
        <span>{x}</span>
        <span>{y}</span>
      </div>
    </div>
</>

在这里插入图片描述
对于下面的JSX,编译之后的virtualDOM如下,可以看到其children为10,而不是一个数组。

   <span>{y}</span>

在这里插入图片描述

4、手写 render

render 把 virtualDOM 变为真实DOM,继续在jsxHandle.js文件中,导出一个render函数,接收两个参数(对于16版本),以下为16版本,跟18版本大差不差。

//React18
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
    <App />
);
//React 16
render(虚拟DOM, document.getElementById('root'));
  • 从virtualDOM 获取 type, props

  • 如果type是string,则是html标签,创建标签(document.createElement(type)

  • 处理props

    • 设置标签上的className、style等属性,通过setAttribute进行
    • children属性,说明有子节点,其子节点可分为文本节点、virtualDOM节点
      • 文本节点:创建文件节点,appendChild到当前元素
      • virtualDOM节点:继续递归执行render
  • 最终将元素appendChildcontainer

export function render(virtualDOM, container) {
    let { type, props } = virtualDOM;
    if (typeof type === "string") {
        // 存储的是标签名:动态创建这样一个标签
        let ele = document.createElement(type);
        // 为标签设置相关的属性 & 子节点
        each(props, (value, key) => {
             console.log(props,value, key)
            // className的处理:value存储的是样式类名
            if (key === 'className') {
                ele.className = value;
                return;
            }
            // style的处理:value存储的是样式对象
            if (key === 'style') {
                each(value, (val, attr) => {
                    ele.style[attr] = 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;
                    }
                    // 子节点又是一个virtualDOM:递归处理
                    render(child, ele);
                });
                return;
            }
            ele.setAttribute(key, value);
        });
        // 把新增的标签,增加到指定容器中
        container.appendChild(ele);
    }
};

其中每次循环的props的值如下:

递归props设置标签上的属性,并处理子节点。所以这里需要一个循环的函数each

  • 判断props是否为对象
  • 判断回调的callback是否为函数
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);
    });
};

在这里插入图片描述
each循环中,props,value,key如下:
在这里插入图片描述

基于传统的for/in循环,会存在一些弊端「性能较差(既可以迭代私有的,也可以迭代公有的);只能迭代“可枚举、非Symbol类型的”属性…」
解决思路:获取对象所有的私有属性「私有的、不论是否可枚举、不论类型」

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

获取所有的私有属性:

let keys = Object.getOwnPropertyNames(arr).concat(Object.getOwnPropertySymbols(arr));

基于ES6中的Reflect.ownKeys代替上述操作「弊端:不兼容IE」:用于获取对象的所有自有属性的键,包括普通的字符串类型属性Symbol 类型属性

  let keys = Reflect.ownKeys(arr);

源码

可以私信我


原文地址:https://blog.csdn.net/qq_38951259/article/details/145149659

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!