美文网首页
你不知道的Virtual DOM(一):Virtual Dom

你不知道的Virtual DOM(一):Virtual Dom

作者: 有赞技术团队 | 来源:发表于2018-10-16 15:19 被阅读8次

    一、前言

    目前最流行的两大前端框架,React 和 Vue,都不约而同的借助 Virtual DOM 技术提高页面的渲染效率。那么,什么是 Virtual DOM ?它是通过什么方式去提升页面渲染效率的呢?本系列文章会详细讲解 Virtual DOM 的创建过程,并实现一个简单的 Diff 算法来更新页面。本文的内容脱离于任何的前端框架,只讲最纯粹的 Virtual DOM 。敲单词太累了,下文 Virtual DOM 一律用 VD 表示。

    这是 VD 系列文章的开篇,后续还会有更多的文章带你深入了解 VD 的奥秘。

    二、VD 是什么

    本质上来说,VD 只是一个简单的JS对象,并且最少包含tagpropschildren三个属性。不同的框架对这三个属性的命名会有点差别,但表达的意思是一致的。它们分别是标签名( tag )、属性( props )和子元素对象( children )。下面是一个典型的 VD 对象例子:

    {
        tag: "div",
        props: {},
        children: [
            "Hello World", 
            {
                tag: "ul",
                props: {},
                children: [{
                    tag: "li",
                    props: {
                        id: 1,
                        class: "li-1"
                    },
                    children: ["第", 1]
                }]
            }
        ]
    }
    

    VD 跟 dom 对象有一一对应的关系,上面的 VD 是由以下的 HTML 生成的

    <div>
        Hello World
        <ul>
            <li id="1" class="li-1">
                第1
            </li>
        </ul>
    </div>
    

    一个 dom 对象,比如li,由tag(li), props({id: 1, class: "li-1"})children(["第", 1])三个属性来描述。

    三、为什么需要 VD

    借助 VD,可以达到有效减少页面渲染次数的目的,从而提高渲染效率。我们先来看下页面的更新一般会经过几个阶段。


    1.png

    从上面的例子中,可以看出页面的呈现会分以下3个阶段:

    • JS计算
    • 生成渲染树
    • 绘制页面

    这个例子里面,JS 计算用了691毫秒,生成渲染树578毫秒,绘制73毫秒。如果能有效的减少生成渲染树和绘制所花的时间,更新页面的效率也会随之提高。
    通过 VD 的比较,我们可以将多个操作合并成一个批量的操作,从而减少 dom 重排的次数,进而缩短了生成渲染树和绘制所花的时间。至于如何基于 VD 更有效率的更新 dom,是一个很有趣的话题,日后有机会将另写一篇文章介绍。

    四、如何实现 VD 与真实 DOM 的映射

    我们先从如何生成 VD 说起。借助 JSX 编译器,可以将文件中的 HTML 转化成函数的形式,然后再利用这个函数生成 VD。看下面这个例子:

    function render() {
        return (
            <div>
                Hello World
                <ul>
                    <li id="1" class="li-1">
                        第1
                    </li>
                </ul>
            </div>
        );
    }
    

    这个函数经过 JSX 编译后,会输出下面的内容:

    function render() {
        return h(
            'div',
            null,
            'Hello World',
            h(
                'ul',
                null,
                h(
                    'li',
                    { id: '1', 'class': 'li-1' },
                    '\u7B2C1'
                )
            )
        );
    }
    

    这里的h是一个函数,可以起任意的名字。这个名字通过 babel 进行配置:

    // .babelrc 文件
    {
      "plugins": [
        ["transform-react-jsx", {
          "pragma": "h"    // 这里可配置任意的名称
        }]
      ]
    }
    

    接下来,我们只需要定义 h 函数,就能构造出 VD

    function flatten(arr) {
        return [].concat.apply([], arr);
    }
    
    function h(tag, props, ...children) {
        return {
            tag, 
            props: props || {}, 
            children: flatten(children) || []
        };
    }
    

    h 函数会传入三个或以上的参数,前两个参数一个是标签名,一个是属性对象,从第三个参数开始的其它参数都是 children。children 元素有可能是数组的形式,需要将数组解构一层。比如:

    function render() {
        return (
            <ul>
                <li>0</li>
                {
                    [1, 2, 3].map( i => (
                        <li>{i}</li>
                    ))
                }
            </ul>
        );
    }
    
    // JSX 编译后
    function render() {
        return h(
            'ul',
            null,
            h(
                'li',
                null,
                '0'
            ),
            /*
             * 需要将下面这个数组解构出来再放到 children 数组中
             */
            [1, 2, 3].map(i => h(
                'li',
                null,
                i
            ))
        );
    }
    

    继续之前的例子。执行 h 函数后,最终会得到如下的 VD 对象:

    {
        tag: "div",
        props: {},
        children: [
            "Hello World", 
            {
                tag: "ul",
                props: {},
                children: [{
                    tag: "li",
                    props: {
                        id: 1,
                        class: "li-1"
                    },
                    children: ["第", 1]
                }]
            }
        ]
    }
    

    下一步,通过遍历 VD 对象,生成真实的 dom

    // 创建 dom 元素
    function createElement(vdom) {
        // 如果 vdom 是字符串或者数字类型,则创建文本节点,比如“Hello World”
        if (typeof vdom === 'string' || typeof vdom === 'number') {
            return doc.createTextNode(vdom);
        }
    
        const {tag, props, children} = vdom;
    
        // 1. 创建元素
        const element = doc.createElement(tag);
    
        // 2. 属性赋值
        setProps(element, props);
    
        // 3. 创建子元素
        // appendChild 在执行的时候,会检查当前的 this 是不是 dom 对象,因此要 bind 一下
        children.map(createElement)
                .forEach(element.appendChild.bind(element));
    
        return element;
    }
    
    // 属性赋值
    function setProps(element, props) {
        for (let key in props) {
            element.setAttribute(key, props[key]);
        }
    }
    

    createElement函数执行完后,dom元素就创建完并展示到页面上了(页面比较丑,不要介意...)。

    2.png

    五、总结

    本文介绍了 VD 的基本概念,并讲解了如何利用 JSX 编译 HTML 标签,然后生成 VD,进而创建真实 dom 的过程。下一篇文章将会实现一个简单的 VD Diff 算法,找出 2 个 VD 的差异并将更新的元素映射到 dom 中去。

    P.S.: 想看完整代码见这里:代码

    参考链接:

    相关文章

      网友评论

          本文标题:你不知道的Virtual DOM(一):Virtual Dom

          本文链接:https://www.haomeiwen.com/subject/szshzftx.html