Virtual DOM

一、Virtual DOM 是什么

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

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

Virtual DOMdom对象有一一对应的关系,上面的 Virtual DOM 是由以下的 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])三个属性来描述。

二、为什么需要Virtual DOM

Virtual DOM 最大的特点是将页面的状态抽象为 JS 对象的形式,配合不同的渲染工具,使跨平台渲染成为可能。如 React 就借助 Virtual DOM 实现了服务端渲染、浏览器渲染和移动端渲染等功能。
借助 Virtual DOM ,可以达到有效减少页面渲染次数的目的,从而提高渲染效率。我们先来看下页面的更新一般会经过几个阶段:

0.jpeg

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

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

这个例子里面,JS 计算用了 691毫秒,生成渲染树 578毫秒,绘制 73毫秒。如果能有效的减少生成渲染树和绘制所花的时间,更新页面的效率也会随之提高

通过 Virtual DOM 的比较,我们可以将多个操作合并成一个批量的操作,从而减少 dom 重排的次数,进而缩短了生成渲染树和绘制所花的时间。

三、如何实现 Virtual DOM 与 真实 DOM 的映射

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

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"    // 这里可配置任意的名称
    }]
  ]
}

babel 会将JSX转化成h函数,状态值会将state当作参数传给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 函数后,最终会得到如下的 Virtual DOM 对象:

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

下一步,通过遍历 Virtual DOM 对象,生成真实的 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]);
    }
}

Virtual DOM 如何更新真实的Dom

使用 Virtual DOM 的框架,一般的设计思路都是页面等于页面状态的映射,即UI = render(state)。当需要更新页面的时候,无需关心 DOM 具体的变换方式,只需要改变state即可,剩下的事情(render)将由框架代劳。我们考虑最简单的情况,当 state 发生变化时,我们重新生成整个 Virtual DOM ,触发比较的操作。上述过程分为以下四步:

  • state 变化,生成新的 Virtual DOM
  • 比较 Virtual DOM 与之前 Virtual DOM 的异同
  • 生成差异对象(patch
  • 遍历差异对象并更新 DOM

差异对象的数据结构是下面这个样子,与每一个 vdom 元素一一对应:

{
    type,
    vdom,
    props: [{
               type,
               key,
               value 
            }]
    children
}

提高渲染性能

渲染数组给数组增加key

用过React或者Vue的朋友都知道在渲染数组元素的时候,编译器会提醒加上 key这个属性,那么key是用来做什么的呢?

在渲染数组元素时,它们一般都有相同的结构,只是内容有些不同而已,比如:

<ul>
    <li>
        <span>商品:苹果</span>
        <span>数量:1</span>
    </li>
    <li>
        <span>商品:香蕉</span>
        <span>数量:2</span>
    </li>
    <li>
        <span>商品:雪梨</span>
        <span>数量:3</span>
    </li>
</ul>

可以把这个例子想象成一个购物车。此时如果想往购物车里面添加一件商品,性能不会有任何问题,因为只是简单的在ul的末尾追加元素,前面的元素都不需要更新:

<ul>
    <li>
        <span>商品:苹果</span>
        <span>数量:1</span>
    </li>
    <li>
        <span>商品:香蕉</span>
        <span>数量:2</span>
    </li>
    <li>
        <span>商品:雪梨</span>
        <span>数量:3</span>
    </li>
     <li>
        <span>商品:橙子</span>
        <span>数量:2</span>
    </li>
</ul>

但是,如果我要删除第一个元素,根据VD的比较逻辑,后面的元素全部都要进行更新的操作。dom结构简单还好说,如果是一个复杂的结构,那页面渲染的性能将会受到很大的影响。

<ul>
    <li>
        <span>商品:香蕉</span>
        <span>数量:2</span>
    </li>
    <li>
        <span>商品:雪梨</span>
        <span>数量:3</span>
    </li>
     <li>
        <span>商品:橙子</span>
        <span>数量:2</span>
    </li>
</ul>

有什么方式可以降低这种性能的损耗呢?

最直观的方法肯定是直接删除第一个元素然后其它元素保持不变了。但程序没有这么智能,可以像我们一样一眼就看出变化。程序能做到的是尽量少的修改元素,通过移动元素而不是修改元素来达到更新的目的。为了告诉程序要怎么移动元素,我们必须给每个元素加上一个唯一标识,也就是key。

<ul>
    <li key="apple">
        <span>商品:苹果</span>
        <span>数量:1</span>
    </li>
    <li key="banana">
        <span>商品:香蕉</span>
        <span>数量:2</span>
    </li>
    <li key="pear">
        <span>商品:雪梨</span>
        <span>数量:3</span>
    </li>
    <li key="orange">
        <span>商品:橙子</span>
        <span>数量:2</span>
    </li>
</ul>

当把苹果删掉的时候,VD里面第一个元素是香蕉,而dom里面第一个元素是苹果。当元素有key属性的时候,框架就会尝试根据这个key去找对应的元素,找到了就将这个元素移动到第一个位置,循环往复。最后VD里面没有第四个元素了,才会把苹果从dom移除。

  • 将所有dom子元素分为有key和没key两组
  • 遍历VD子元素,如果VD子元素有key,则去查找有key的分组;如果没key,则去没key的分组找一个类型相同的元素出来
  • diff一下,得出是否更新元素的类型
  • 如果是更新元素且子元素不是原来的,则移动元素
  • 最后清理删除没用上的dom子元素

setState异步更新

为了减少不必要的渲染,提高性能,React并不是在我们每次setState的时候都进行渲染,而是将一个同步操作里面的多个setState进行合并后再渲染,给人异步渲染的感觉。

总结

reactvue框架 提升开发效率是因为框架帮我们完成了数据和视图之间的绑定,使得开发者只需要关注数据的变化,减少开发者各种不必要的DOM操作达到性能提升,而数据到视图的映射利用了Virtual DOM这一思路来提升性能。
Virtual DOM只是一种利用数据结构的思想,把复杂的,真实的DOM树转化为轻量的,速度更快的JS Object,通过优化的diff算法后再把变化应用到真实的DOM树上。

前端大宝剑

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342

推荐阅读更多精彩内容

  • 前言 React 好像已经火了很久很久,以致于我们对于 Virtual DOM 这个词都已经很熟悉了,网上也有非常...
    NARUTO_86阅读 17,144评论 6 65
  • 变化这件事谈论页面的变化之前,咱们先看下数据和页面(视觉层面的页面)的关系。数据是隐藏在页面底下,通过渲染展示给用...
    Www刘阅读 442评论 0 1
  • 声明:本文copy自深度剖析:如何实现一个 Virtual DOM 算法-戴嘉华,留做记录已做日后温习 目录: 1...
    会飞小超人阅读 280评论 0 0
  • 目录: 1 前言 2 对前端应用状态管理思考 3 Virtual DOM 算法 4 算法实现4.1 步骤一:用JS...
    RThong阅读 388评论 0 2
  • “他的一瞥,增加了我的姿色 我将它占为己有 我幸福地吞下一颗星辰” 递与他星辰 递与他寂寞 我允许自己被他想入非非...
    Dianee阅读 615评论 0 3