乐趣区

关于javascript:浅谈-Virtual-DOM

前言

“Virtual Dom 的劣势是什么?”这是一个常见的面试问题,然而答案真的仅仅是简略粗犷的一句“间接操作 dom 和频繁操作 dom 的性能很差”就完事了吗?如果是这样的话,无妨持续深刻地问几个问题:

  • 间接操作 Dom 的性能为什么差?
  • Virtual Dom 到底是指什么?它是如何实现的?
  • 为什么 Virtual Dom 可能防止间接操作 dom 引起的问题?

如果发现自己对这些问题不 (yi) 太(lian)确 (meng) 定(bi),那么无妨往下读一读。

注释

Virtual Dom,也就是虚构的 Dom, 无论是在 React 还是 Vue 都有用到。它自身并不是任何技术栈所独有的设计,而是一种设计思路,或者说设计模式。

DOM

在介绍虚构 dom 之前,首先来看一下与之绝对应的实在 Dom:

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

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

形如以下的 html 代码,

<!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),每个节点都蕴含着对象,蕴含一些节点属性。这就是 基于对象来示意文档

其次,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) 关联起来

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 进行哪些特定更改,并以针对性和优化的形式进行更改.

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

将军 1:我感觉三队的士兵应该往东边挪动 200 米,侧翼潜伏,而后传令官跑去告诉三队的士兵,吭哧吭哧跑了 200 米;

将军 2: 我感觉四队的士兵应该往西边挪动 200 米,和三队造成合围之势,而后传令官持续告诉,四队的士兵也持续奔跑。

将军 3:我感觉潜伏的间隔太远了,近一点比拟好,两队各向两头挪动 100 米吧。

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

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

  • 先在沙盘上进行模仿排列,
  • 等到得出现实方阵 之后,最初再告诉到手下的士兵进行对应的调整,

这也就是 Virtual DOM 要做的事。

Virtual DOM 的简化实现

那么 Virtual DOM 大略是什么样呢?还是依照后面的 html 文件,对应的 virtual dom 大略长这样(不代表理论技术栈的实现,只是体现外围思路):

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 自身的概念。

欢送大家关注专栏,也心愿大家 对于青睐的文章,可能不吝点赞和珍藏,对于行文格调和内容有任何意见的,都欢送私信交换。

退出移动版