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

354

欢迎关注我的公众号睿Talk,获取我最新的文章:
clipboard.png

一、前言

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

这是VD系列文章的开篇,以下是本系列其它文章的传送门:
你不知道的Virtual DOM(一):Virtual Dom介绍
你不知道的Virtual DOM(二):Virtual Dom的更新
你不知道的Virtual DOM(三):Virtual Dom更新优化
你不知道的Virtual DOM(四):key的作用
你不知道的Virtual DOM(五):自定义组件
你不知道的Virtual DOM(六):事件处理&异步更新

二、VD是什么

本质上来说,VD只是一个简单的JS对象,并且最少包含tag、props和children三个属性。不同的框架对这三个属性的命名会有点差别,但表达的意思是一致的。它们分别是标签名(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 最大的特点是将页面的状态抽象为 JS 对象的形式,配合不同的渲染工具,使跨平台渲染成为可能。如 React 就借助 VD 实现了服务端渲染、浏览器渲染和移动端渲染等功能。

此外,在进行页面更新的时候,借助VD,DOM 元素的改变可以在内存中进行比较,再结合框架的事务机制将多次比较的结果合并后一次性更新到页面,从而有效地减少页面渲染的次数,提高渲染效率。我们先来看下页面的更新一般会经过几个阶段。
clipboard.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元素就创建完并展示到页面上了(页面比较丑,不要介意...)。

clipboard.png

五、总结

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

P.S.: 想看完整代码见这里,如果有必要建一个仓库的话请留言给我:代码

你可能感兴趣的

18 条评论
问心667 · 2018年09月20日

感谢大佬,我有两个疑惑,求解。
1、【继续之前的例子。执行h函数后,最终会得到如下的VD对象:】处,下面的代码【children: ["第", 1]】,不应该是【children: ["第1"]】吗?
2、【appendChild在执行的时候,会检查当前的this是不是dom对象,因此要bind一下】处,难道element还有可能不是DOM对象,bind没有必要存在的吧!

回复

0
  1. 当1被包裹在{}中就会解析成这样,如{i}
  2. 执行element.appendChild的时候,this会取当前上下文的this,全局作用域下是window,因此要bind一下
dabai 作者 · 2018年09月20日
0

@dabai
1、【继续之前的例子】 指的应该是 【 'u7B2C1' 】 吧 这应该是整个作为一个文本节点传参的 【 'u7B2C1' 】所以返回的也是 ['第1'],不应该是 ['第', '1']
2、通过 createElement 方法遍历VD对象的时候,递归的时候,每一个element变量都是唯一不同的当前的DOM对象啊【就是你第1补创建的元素】,this怎么会指向window,应该就是指向的 element吧

问心667 · 2018年09月20日
0

@dabai
sorry
关于您的回答2:
或许是我没理解到,或许有些知识我还不知。是不是使用 appendChild 方法的时候,必须要保证函数作用域中, this 指向的是调用appendChild方法的DOM对象?
关于我的追问2:
修正,this指向是window。appendChild方法难道不是只需要有调用它的DOM元素,然后在里面插入节点就好了吗?与this有何关系?

问心667 · 2018年09月20日
Zhuxy · 1月1日

compiled.js是什么?jsx编译器?

回复

0

babel后的文件

dabai 作者 · 1月2日
0

@dabai 是 vdom_v1.js babel后的?

Zhuxy · 1月2日
0

是的

dabai 作者 · 1月3日
雷帝嘎嘎 · 4月1日

麻烦发一下完整代码,我想学习一下

回复

0

文章最后有

dabai 作者 · 4月2日
0

点击‘代码’后打开的网址无法访问。eg : https://gist.github.com/dicke...

qinqinlv · 4月9日
0

可以访问的,不确定是否要翻墙。留个邮箱,我发你

dabai 作者 · 4月11日
GotoLu · 7月3日

请问jsx编译时为啥要为children生成一个新的Array

回复

载入中...