Skip to content

虚拟DOM

DOM的工作原理

  • 我们写的代码是js代码,但是浏览器引擎是使用C++写的。
  • 通过 WebIDL(Web Interface Definition Language) Web接口定义语言,这个就是定义浏览器和JS之间是如何进行通信的。
  • 通过 WebIDL 浏览器开发者可以描述哪些类和方法能够被JS访问,以及这些方法应该如何映射到JS中的对象和方法。
  • 工作流程:
  • 首先JS引擎执行JS代码,如果JS引擎发现是要创建DOM节点,会将其识别成一个API来调用,然后向浏览器底层(渲染引擎)发出请求,由浏览器底层(渲染引擎)负责创建这个DOM元素。浏览器底层创建完DOM元素之后,需要向调用端返回一个结果。

本质

  • 最初虚拟 DOM 是由 React 团队提出的: 虚拟DOM是一种编程概念。
  • 在这个概念里, UI以一种理想化,或者说'虚拟的'表现形式被保存在内存中。
  • 理论上讲,无论使用什么样的结构,只要能将文档结构展示出来,那么这种结构就是一种虚拟DOM,但实际上也就只有JS对象适合干这种事情。
  • 在Vue中可以调用一个名叫h的函数,该函数返回的就是虚拟DOM。
       示例:
       const vnode =  h('div', { class: 'bar', innerHTML: 'hello' })
       结果:
       {
            tag: "div",
            data: {
                class: "bar",
                domProps: {
                innerHTML: "hello"
                }
            },
            children: undefined,
            text: undefined,
            elm: undefined,
            key: undefined,
            componentOptions: undefined,
            context: VueComponentInstance,
            isComment: false,
            isStatic: false
        }
  • 由此可以看出虚拟DOM的本质就是一个普通的JS对象。

为什么要使用虚拟DOM

在最早期的时候,前端用过手动操作DOM节点来编写代码。
    // 创建一个新的<div>元素
    var newDiv = document.createElement("div");
    // 给这个新的<div>添加一些文本内容
    var newContent = document.createTextNode("Hello, World!");
    // 把文本内容添加到<div>中
    newDiv.appendChild(newContent);
    // 最后,把这个新的<div>添加到body中
    document.body.appendChild(newDiv);


    // 假设我们有一个已存在的元素ID为'myElement'
    var existingElement = document.getElementById("myElement");
    // 更新文本内容
    existingElement.textContent = "Updated content here!";
    // 更新属性,例如改变样式
    existingElement.style.color = "red";


    // 假设我们要删除ID为'myElement'的元素
    var elementToRemove = document.getElementById("myElement");
    // 获取父节点
    var parent = elementToRemove.parentNode;
    // 从父节点中移除这个元素
    parent.removeChild(elementToRemove);


    // 创建新节点
    var newNode = document.createElement("div");
    newNode.textContent = "这是新的文本内容";
    // 假设我们想把这个新节点插入到id为'myElement'的元素前面
    var referenceNode = document.getElementById("myElement");
    referenceNode.parentNode.insertBefore(newNode, referenceNode);

上面的代码从编程范式的角度看属于命令式编程,这种编程方式的性能一定是最高的。

所以创建一个div的DOM节点,没有哪种方式是比 document.createElement("div") 这句代码还要高的。

虽然这种性能是最高的,但是在实际开发中,往往会倾向于更加方便的方式。

比如使用innerHTML的方式。

var app = document.getElementById("app");

app.innerHTML += `
  <div class="message">
    <div class="info">
      <span>张三</span>
    </div>
    <div class="btn">
      <a href="#" class="removeBtn" _id="1">删除</a>
    </div>
  </div>`;

虽然这种方式在性能上差一些,但是写起来会轻松一些。

原因就是使用innerHTML涉及到了两个层面的计算:

  • 解析字符串(JS层面)
  • 创建对应的DOM节点(DOM层面)

使用虚拟DOM也涉及到两个层面的计算:

  • 创建JS对象(虚拟DOM,JS层面)
  • 根据JS对象创建对应的DOM节点(DOM层面)

JS层面的计算和DOM层面的计算是完全不同的。

实际上无论使用innerHTML还是使用虚拟DOM,在初始化时性能相差无几。虚拟DOM的快实际上是在更新的时候。

更新:

使用innerHTML时涉及的计算层面:

  • 销毁所有旧的DOM(DOM层面)
  • 解析模板字符串(JS层面)
  • 重新创建所有的DOM节点(DOM层面)

如果使用虚拟DOM,只会涉及到两个层面的计算:

  • 使用diff计算出更新的节点(JS层面)
  • 更新必要的节点(DOM层面)

所以常说的虚拟DOM快 是有前提的

  • 首先看和谁比较
    • 如果和原生JS操作DOM相比,那无疑肯定是原生JS操作性能高,因为虚拟DOM多了一层计算。
    • 如果和innerHTML相比,初始化时两者差距并不大,但是在更新时虚拟DOM会比innerHTML性能更高。

总结一句话:

  • 使用虚拟DOM是为了防止组件在重渲染时导致的性能恶化。