浅谈 Virtual DOM

前言

“Virtual Dom 的优点是什么?” 这是一个常见的面试问题,可是答案真的仅仅是简单粗暴的一句“直接操做dom和频繁操做dom的性能不好”就完事了吗?若是是这样的话,不妨继续深刻地问几个问题:javascript

  • 直接操做Dom的性能为何差?
  • Virtual Dom究竟是指什么?它是如何实现的?
  • 为何Virtual Dom可以避免直接操做dom引发的问题?html

    image.png

若是发现本身对这些问题不(yi)太(lian)确(meng)定(bi),那么不妨往下读一读。vue

正文

Virtual Dom,也就是虚拟的Dom, 不管是在React仍是Vue都有用到。它自己并非任何技术栈所独有的设计,而是一种设计思路,或者说设计模式。java

DOM

在介绍虚拟dom以前,首先来看一下与之相对应的真实Dom:node

DOM(Document Object Model)的含义有两层:react

  1. 基于对象来表示的文档模型(the object-based representation);
  2. 操做这些对象的API;

形如如下的html代码,web

<!DOCTYPE html>
<html lang="en">
<head>
    <title>Document</title>
</head>
<body>
    <h1>Learning Virtual Dom</h1>
    <ul class="list">
        <li class="list-item">List item</li>
    </ul>
</body>
</html>

根据DOM会被表示为以下一棵树: 树的每一个分支的终点都是一个节点(node),每一个节点都包含着对象,包含一些节点属性。 这就是基于对象来表示文档
image.png面试

其次,DOM容许咱们经过一些的api对文档进行操做,例如:设计模式

const listItemOne = document.getElementsByClassName("list-item")[0]; // 获取节点
listItemOne.textContent = "List item one"; // 修改对应的文本内容
const listItemTwo = document.createElement("li"); // 建立一个元素对象
listItemTwo.classList.add("list-item"); // 添加子元素
listItemTwo.textContent = "List item two";
list.appendChild(listItemTwo);

简而言之。DOM的做用就是把web页面和脚本(一般是指Javascript)关联起来api

DOM操做带来的性能问题

那么原生的DOM操做存在哪些问题呢?在此还须要了解到浏览器工做的一些流程,一般来讲,一个页面的生成须要经历如下步骤:

  1. 解析HTML,产出对应的DOM树;
  2. 解析CSS, 生成对应的CSS树;
  3. 将1和2的结果结合生成一棵render树;
  4. 生成页面的布局排列(flow)
  5. 将布局绘制到显示设备上(paint)

其中第4步和第5步其实就是常说的页面渲染,而渲染的过程除了在页面首次加载时发生,在后续交互过程当中,DOM操做也会引发从新排列和从新绘制,渲染是须要较高性能代价的,尤为是重排的过程。

因此常见的优化思路都会提到一点: 为了尽量减小重绘和重排次数,尽可能把改变dom的操做集中在一块儿,由于写入操做会触发重绘或者重排,而且浏览器的渲染队列机制是:当某个操做触发重排或重绘时,先把该操做放进渲染队列,等到队列中的操做到了必定的数量或者到了必定的时间间隔时,浏览器就会批量执行。因此集中进行dom操做能够减小重绘重排次数。

另外一方面,关于DOM操做的影响范围问题:因为浏览器是基于流式布局的,因此一旦某个元素重排,它的内部节点会受到影响,而外部节点(兄弟节点和父级节点等等)是有可能不受影响的,这种局部重排引发的影响比较小,因此也须要尽量地每次只改动最须要的节点元素。

Virtual DOM概览

Virtual DOM 就是为了解决上面这个问题而生的,它为咱们操做dom提供了一种新的方式。

virtual DOM 的本质就是真实dom的一个副本,无需使用DOM API,就能够频繁地操做和更新此副本。 对虚拟DOM进行全部更新后,咱们能够查看须要对原始DOM进行哪些特定更改,并以针对性和优化的方式进行更改.

image.png

这个思路能够参照行军打仗时的沙盘,沙盘的一个做用就是模拟军队的排列分布。设想一下不借助沙盘时的场景:

将军1: 我以为三队的士兵应该往东边移动200米,侧翼埋伏,而后传令官跑去通知三队的士兵,吭哧吭哧跑了200米;

将军2: 我以为四队的士兵应该往西边移动200米,和三队造成合围之势,而后传令官继续通知,四队的士兵也继续奔跑。

将军3:我以为埋伏的距离太远了,近一点比较好, 两队各向中间移动100米吧。

而后可怜的士兵们继续来回跑....

image.png

在这个过程里每次行军移动都要带来大量的开销,每次都直接用实际行动执行还在商讨中的指令,成本是很高的。实际上在将军们探讨商量布阵排列时,能够

  • 先在沙盘上进行模拟排列,
  • 等到得出理想方阵以后,最后再通知到手下的士兵进行对应的调整,

这也就是 Virtual DOM 要作的事。

Virtual DOM 的简化实现

那么 Virtual DOM大概是什么样呢? 仍是按照前面的html文件,对应的virtual dom大概长这样(不表明实际技术栈的实现,只是体现核心思路):

image.png

const vdom = {
    tagName: "html",// 根节点
    children: [
        { tagName: "head" },
        {
            tagName: "body",
            children: [
                {
                    tagName: "ul",
                    attributes: { "class": "list" },
                    children: [
                        {
                            tagName: "li",
                            attributes: { "class": "list-item" },
                            textContent: "List item"
                        } // end li
                    ]
                } // end ul
            ]
        } // end body
    ]
} // end html

咱们用一棵js的嵌套对象树表示出了dom树的层级关系以及一些核心属性,children表示子节点。
在前文咱们用原生dom给ul作了一些更新,如今使用Virtual Dom来实现这个过程:

  1. 针对当前的真实DOM复制一份virtual DOM,以及指望改动后的virtual DOM;

    const originalDom = {
    tagName: "html",// 根节点
    children: [
    //省略中间节点
      {
         tagName: "ul",
         attributes: { "class": "list" },
         children: [
             {
                 tagName: "li",
                 attributes: { "class": "list-item" },
                 textContent: "List item"
             }
         ]
      }
    ],
    }
    const newDom = {
    tagName: "html",// 根节点
    children: [
      //省略中间节点
       {
         tagName: "ul",
         attributes: { "class": "list" },
         children: [
             {
                 tagName: "li",
                 attributes: { "class": "list-item" },
                 textContent: "List item one" //改动1,第一个子节点的文本 
             },
             {// 改动2,新增了第二个节点
                 tagName: "li",
                 attributes: { "class": "list-item" },
                 textContent: "List item two"
             }
         ]
      }
     ], 
    };
  2. 比对差别

    const diffRes = [
     {
       newNode:{/*对应上面ul的子节点1*/},
       oldNode:{/*对应上面originalUl的子节点1*/},
     },
     {
       newNode:{/*对应上面ul的子节点2*/},//这是新增节点,因此没有oldNode
     },
    ]
  3. 收集差别结果以后,发现只要更新list节点,,伪代码大体以下:

    const domElement = document.getElementsByClassName("list")[0];
    diffRes.forEach((diff) => {
     const newElement = document.createElement(diff.newNode.tagName);
     /* Add attributes ... */
     
     if (diff.oldNode) {
         // 若是存在oldNode则替换
         domElement.replaceChild(diff.newNode, diff.index);
     } else {
         // 不存在则直接新增
         domElement.appendChild(diff.newNode);
     }
    })

    固然,实际框架诸如vuereact里的diff过程不仅是这么简单,它们作了更多的优化,例如:

对于有多个项的ul,往其中append一个新节点,可能要引发整个ul全部节点的改动,这个改动成本过高,在diff过程若是遇到了,可能会换一种思路来实现,直接用js生成一个新的ul对象,而后替换原来的ul。这些在后续介绍各个技术栈的文章(可能)会详细介绍。

能够看到,Virtual DOM的核心思路:先让预期的变化操做在虚拟dom节点,最后统一应用到真实DOM中去,这个操做必定程度上减小了重绘和重排的概率,由于它作到了:

  1. 将实际dom更改放在diff过程以后, diff的过程有可能通过计算,减小了不少没必要要的改变(如同前文将军3的命令一出,士兵的实际移动其实就变少了);
  2. 对于最后必要的dom操做,也集中在一块儿处理,贴合浏览器渲染机制,减小重排次数;

    小结:回答开头的问题

如今咱们回到开篇的问题--“Virtual Dom 的优点是什么?”

在回答这道题以前,咱们还须要知道:

  1. 首先,浏览器的DOM 引擎、JS 引擎 相互独立,可是共用主线程;
  2. JS 代码调用 DOM API 必须 挂起 JS 引擎,激活 DOM 引擎,DOM 重绘重排后,再激活 JS 引擎并继续执行;
  3. 如有频繁的 DOM API 调用,浏览器厂商不作“批量处理”优化,因此切换开销和重绘重排的开销会很大;

而Virtual Dom 最关键的地方就是把dom须要作的更改,先放在js引擎里进行运算,等收集到必定期间的全部dom变动时,这样作的好处是:

  1. 减小了dom引擎和js引擎的频繁切换带来的开销问题;
  2. 可能在计算比较后,最终只须要改动局部,能够较少不少没必要要的重绘重排;
  3. 把必要的Dom操做尽可能集中在一块儿作,减小重排次数

总结

本文从一个常见面试问题出发,介绍了Dom 和Virtual Dom的概念,以及直接操做Dom可能存在的问题,经过对比来讲明Virtual Dom的优点。对于具体技术栈中的Virtual Dom diff过程和优化处理的方式,没有作较多说明,更专一于阐述Virtual Dom自己的概念。

欢迎你们关注专栏,也但愿你们对于喜好的文章,可以不吝点赞和收藏,对于行文风格和内容有任何意见的,都欢迎私信交流。

相关文章
相关标签/搜索