什么是虚拟 dom?虚拟 dom 的具体作用?

368 阅读6分钟

在前端开发领域,性能优化始终是重中之重。虚拟 DOM(Virtual DOM)作为现代前端框架的核心技术之一,像 Vue,深刻地影响着我们构建用户界面的方式。那么,虚拟 DOM 究竟是什么?它在 Vue 中又有着怎样的作用呢?

一、什么是虚拟 DOM

1. 概念

虚拟 DOM 是真实 DOM 在 JavaScript 中的抽象表示,是一棵轻量级的 JavaScript 对象树,包含创建真实 DOM 节点所需的信息,如标签名、属性、子节点等 。在 Vue 中,我们编写的模板语法最终会被编译成虚拟 DOM。例如,有如下模板

<template>
  <div>
    <span>hello world</span>
  </div>
</template>

// 上面的模板最终会被编译成下面的代码
const obj = {
  tag: "div",
  children: [{ tag: "span", children: "hello world" }],
};
// ☝️ 这就是虚拟 dom 
//  tag 为标签名 children 如果是数组就是子节点,如果是字符串就是内容

 

二、 为什么要用虚拟 Dom?作用是什么?

知道了虚拟 Dom 是什么东西,那么为什么我们要用他呢?我们直接用真实的 Dom,不是可以省去把模板转为虚拟 Dom 和把虚拟 Dom 渲染到页面两个步骤吗?其实用虚拟 Dom,一共有两个原因

1.减少心智负担

我们使用真实 Dom 的时候,我们应该怎么用?

const app = document.querySelector("#app");
app.innerText = 'hello world';

如果每一行都要那么写,那废了,手都要写断。所以虽然真实 Dom 性能好,但是很麻烦。

虚拟 Dom 使你可以直接在模板里书写变量,使你代码写得更舒服

2.减少频繁的操作Dom,优化性能

为什么要减少频繁操作 Dom?可以看下面的数据对比 👇

// 使用Console API进行性能标记
console.time("obj dom");
const app = [];
for (let i = 0; i < 10000; i++) {
  const div = { tag: "div" };
  app.push(div);
}
console.timeEnd("obj dom"); // 输出执行时间

console.time("real dom");
const app2 = document.querySelector("#app");
for (let i = 0; i < 10000; i++) {
  const div = document.createElement("div");
  app2.appendChild(div);
}
console.timeEnd("real dom"); // 输出执行时间
// obj dom: 0.22998046875 ms
// real dom: 6.163818359375 ms

可以看到,创建真实 Dom 会比创建 Js 对象慢很多,操作也是如此。假如后台返回了数据,我们再像上面一行一行的全量更新 Dom,因为我们不知道哪些 Dom 有变化,哪些没变化,所以我们全部都得更新。或者因为短时间多次修改要多次更新 Dom,这样就会浪费较多的时间。基于这一点,Diff 算法就诞生了。

当把模板转换为虚拟 Dom,我们就能使用 Diff 算法,最大程度的减少操作 Dom 的次数和范围

三、Vue 中虚拟 DOM 源码逻辑模拟

下面通过一些简单的逻辑,梳理一下虚拟 Dom 的具体使用

<template>
  <div id="app">
    <p>{{ message }}</p>
    <button @click="changeMessage">Change Message</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      message: 'Hello, Vue Virtual DOM'
    };
  },
  methods: {
    changeMessage() {
      this.message = 'New Message';
    }
  }
};
</script>

 
在这个示例中,当点击按钮触发 changeMessage 方法时,message 数据发生变化。Vue 内部会:

  • 生成新的虚拟DOM :根据新的 message 值生成新的虚拟 DOM 树。
  • Diff算法对比 :将新的虚拟 DOM 树与旧的进行对比,计算出差异。
  • 更新真实DOM :仅将差异部分应用到真实 DOM 上,更新

    标签的文本内容,而不会重新渲染整个

知道了什么是虚拟 Dom,以及虚拟 Dom 的作用是什么,那我们可以最后来扩展一下。Vue 是如何把模板代码转换为虚拟 Dom 的。

1. 模板解析

Vue 首先会对用户编写的模板字符串进行解析,将其转换为抽象语法树(AST)。这一步骤借助了 parse 函数

function parse(template) {
  let index = 0;
  let root = null;
  let currentParent = null;
  const stack = [];

  // 解析开始标签
  function parseStartTag() {
          const start = template.indexOf("<", index);
          if (start === -1) return null;
          index = start + 1;
          const end = template.indexOf(">", index);
          const tagName = template.slice(index, end);
          index = end + 1;

          const node = {
            type: 1,
            tag: tagName,
            children: [],
          };
          return node;
        }

  // 解析文本内容
  function parseText() {
          const start = index;
          const end = template.indexOf("<", index);
          if (end === -1) {
            const text = template.slice(index);
            index = template.length;
            return text.trim() ? { type: 3, text } : null;
          }
          const text = template.slice(index, end);
          index = end;
          return text.trim() ? { type: 3, text } : null;
        }

  while (index < template.length) {
          const textNode = parseText();
          if (textNode) {
            if (currentParent) {
              currentParent.children.push(textNode);
            }
            continue;
          }

          const startTag = parseStartTag();
          if (startTag) {
            if (!root) {
              root = startTag;
            }
            if (currentParent) {
              currentParent.children.push(startTag);
            }
            stack.push(startTag);
            currentParent = startTag;
          }
        }

  return root;
}

// 测试示例
const template = "<div>Hello, World</div>";
const ast = parse(template);
console.log(ast);

parse 函数会对模板字符串进行词法分析和语法分析。它会扫描模板字符串,识别出其中的标签、属性、文本等元素,并将它们转换为AST节点 。函数省略了很多逻辑,真实的逻辑需要考虑更多的东西,了解个基本功能就行。

2. 生成抽象语法树(AST)

解析完成后,会得到一个抽象语法树(AST),这是一个树形结构,它包含了模板的所有信息,如标签名、属性、子节点等。

{
    "type": 1,
    "tag": "div",
    "attrsList": [],
    "attrsMap": {},
    "children": [
        {
            "type": 3,
            "text": "Hello, World"
        }
    ]
}
  • type:表示节点的类型,1 代表元素节点,2 代表带有表达式的文本节点,3 代表普通文本节点。
  • tag:标签名,这里是 div。
  • attrsList 和 attrsMap :存储节点的属性信息。
  • children:存储子节点信息,这里包含一个带有表达式的文本节点。

3. 将 AST 转换为渲染函数

Vue 会将生成的 AST 转换为渲染函数。这一步骤由 generate 函数完成

import { generate } from 'vue-template-compiler';

const code = generate(ast);
// code
// {
//    render: 'with(this){return _c('div',[_v("Hello, World")])}',
//    staticRenderFns: []
// }
// _c 是 Vue 内部的创建元素函数,用于创建 VNode(虚拟节点)。
// _v 是 Vue 内部的创建文本节点函数,用于创建文本 VNode。
const renderFunction = new Function(`with(this){return ${code.render}}`);
// renderFunction 结果示例:
// {
//     "tag": "div",
//     "props": null,
//     "children": [
//         "Hello, World"
//     ]
// }
  • generate 函数会根据 AST 生成渲染函数的代码字符串。
  • new Function 会将代码字符串转换为可执行的 JavaScript 函数,这个函数就是渲染函数。渲染函数会返回虚拟 Dom。

generate 涉及了很多东西,比较复杂,这里就不贴了,感兴趣的可以去 Vue 源码 src/compiler/codegen/index.js 目录看一看,保证你看了不想看。这里我们只要知道他会把一个 AST 解析成虚拟 Dom 即可。

4. 最后再将虚拟 Dom 渲染成真实 Dom

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>

  <body>
    <script>
      // 虚拟 DOM 对象
      const vnode = {
        tag: "div",
        props: null,
        children: ["Hello, World"],
      };

      // 将虚拟 DOM 转换为真实 DOM 的函数
      function renderVNode(vnode) {
        // 如果 vnode 是字符串,创建文本节点
        if (typeof vnode === "string") {
          return document.createTextNode(vnode);
        }

        // 创建元素节点
        const el = document.createElement(vnode.tag);

        // 设置属性
        if (vnode.props) {
          for (const key in vnode.props) {
            el.setAttribute(key, vnode.props[key]);
          }
        }

        // 处理子节点
        if (vnode.children) {
          vnode.children.forEach((child) => {
            const childEl = renderVNode(child);
            el.appendChild(childEl);
          });
        }

        return el;
      }

      // 调用 renderVNode 函数将虚拟 DOM 转换为真实 DOM
      const realDom = renderVNode(vnode);

      // 将真实 DOM 添加到页面上
      document.body.appendChild(realDom);
    </script>
  </body>
</html>

老规矩,示例代码。实际情况要更加复杂。

四、总结

虚拟 DOM 通过高效的 Diff 算法和最小化 DOM 操作,极大地提升了页面性能和开发效率。未来将会着重解析剩下的部件,慢慢完善。