JSX底层渲染机制「创建真实DOM」

238 阅读3分钟

上一篇文章说到了JSX的渲染机制流程,以及虚拟dom怎么形成的,这篇就讲一下如何把虚拟dom变成真实的dom

把构建的virtualDOM渲染为真实DOM

    真实DOM:浏览器页面中,最后渲染出来,让用户看见的DOM元素!!

    基于ReactDOM中的render方法处理的!!

      v16

      ReactDOM.render(

        <>...</>,

        document.getElementById('root')

      );



      v18

      const root = ReactDOM.createRoot(document.getElementById('root'));

      root.render(

        <>...</>

      );



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

接下来就是创建真实DOM,遍历DOM节点的属性,我们这次只考虑type和props

image.png 基于传统的for/in循环,会存在一些弊端「性能较差(既可以迭代私有的,也可以迭代公有的);只能迭代“可枚举、非Symbol类型的”属性...」

一般来讲,内置的属性都是不可枚举的「枚举:可以被列举,例如被for/in、Object.keys等列举出来的」;自定义的属性都是可枚举的!!
但是我们可以设定成员的枚举性!! Object.defineProperty()

例:

let arr = [10,20];
console.log(arr);

image.png
可以看到,10,20以及length都被打印出来了
那在加个其他属性呢?

Array.prototype.BB = 200;
let arr = [10,20];
arr[Symbol('AA')] = 100;
console.log(arr);

image.png
同样可以打印出来
那此时用for in循环一下试试

Array.prototype.BB = 200;
let arr = [10,20];
arr[Symbol('AA')] = 100;
console.log(arr);

for (let key in arr) {
  console.log(key)
}

image.png
可以看到,可枚举的无论私有公有的属性都遍历出来了,但是非Symbol类型的属性没有拿到,这就是他的弊端。
这时就要变一下获取方法了。
解决思路:获取对象所有的私有属性「私有的、不论是否可枚举、不论类型」

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

  • Object.getOwnPropertySymbols(arr) -> 获取Symbol类型的私有属性


获取所有的私有属性:

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

image.png
或者这样太麻烦,要写一大堆,展示第二种方法
可以基于ES6中的Reflect.ownKeys代替上述操作「弊端:不兼容IE」

let keys = Reflect.ownKeys(arr);

image.png
那以后,就可以用这种方式代替for in循环了

let keys = Reflect.ownKeys(arr);
keys.forEach(key=>{
  console.log(key,arr[key]);
})

image.png
这时就可以封装我们的each获取虚拟Dom属性的方法了

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);

    });

};
/* render:把虚拟DOM变为真实DOM */

export function render(virtualDOM, container) {

    let { type, props } = virtualDOM;

    if (typeof type === "string") {

        // 存储的是标签名:动态创建这样一个标签

        let ele = document.createElement(type);

        // 把新增的标签,增加到指定容器中

        container.appendChild(ele);

    }

};

下面就是设置DOM的属性了,这里提一下设置DOM的属性需要注意的点
我们一般设置属性都是直接 加"."

image.png
但是此时结构上并没有

image.png
但是往里面加内联样式是可以的

image.png

image.png
虽然这样结构中没有,但是在堆内存中是存在的

image.png
另外一种方式是setAttribute

image.png
可以看到结构上是有了,但是堆内存中没有 image.png

image.png
由此可以总结

0ECC98148FE2740BCCD4E18E90F527E1.png 下面我们就可以设置DOM的属性了

/* render:把虚拟DOM变为真实DOM */

export function render(virtualDOM, container) {

    let { type, props } = virtualDOM;

    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) => {

                    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);

    }

};