Vue之虚拟dom

586 阅读6分钟

1. 什么是虚拟dom?

虚拟dom本质上就是一个普通的JS对象,用于描述视图的界面结构

例子

<div id="app">
    <h1>hello vue</h1>
</div>
{
    tag:"div",
    attrs: {
        id: "app"
    },
    children:[
        tag:"h1",
        attrs: undefined,
        children:[
            tag:undefined,
            text:"hello vue"
        ]
    ]
}

在vue中,每个组件都有一个render函数(即使没有直接的,也有间接生成的)

  • 有render函数
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div id="app">
      <h1>{{title}}</h1>
    </div>

    <script src="./vue.min.js"></script>
    <script>
      var div1 = document.getElementById('app');
      var vm = new Vue({
        // 组件配置
        el:"#app",
        data: {
          title: 'Hello Vue!!!',
        },
        template: '<ul><li>{{title}}</li></ul>',
        render(h) {
          // 目标,创建虚拟dom:这个组件到底要显示啥
          // h是一个函数,专门用于创建虚拟dom
          console.log('render');
          var vnode = h(
            'div',
            {
              // 虚拟dom的一些配置
              attrs: {
                id: 'app',
              },
            },
            // 子元素
            [h('h1', `标题:${this.title}`)]
          );
          console.log(vnode);
          return vnode;
        },
      });
    </script>
  </body>
</html>
  • 没有render,找template,并且有template;

  • 没有render,没有template,找el.outerHTML

  • -

  • 编译template, 变成一个render函数

每个render函数都会返回一个虚拟dom树,这也就意味着每个组件都对应一棵虚拟DOM树

2. 为什么需要虚拟dom?

每次视图依赖的数据变化,那么重新执行render函数

vue中,渲染视图会调用render函数,这种渲染不仅发生在组件创建时,同时发生在视图依赖的数据更新时。

如果在渲染时,直接使用真实DOM,由于真实DOM的创建、更新、插入等操作会带来大量的性能损耗,从而就会极大的降低渲染效率。
因此,vue在渲染时,使用虚拟dom来替代真实dom,主要为解决渲染效率的问题。

做一个创建1千万个对象和dom元素的时间对比

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <script>
      var times = 10000000;
      console.time('js object');
      for (var i = 0; i < times; i++) {
        var obj = {
          tag: 'div',
          attrs: {
            class: 'asdfasdf',
          },
        };
      }
      console.timeEnd('js object');

      console.time('html object');
      for (var i = 0; i < times; i++) {
        var obj = document.createElement('div');
      }
      console.timeEnd('html object');
    </script>
  </body>
</html>
js object: 103.89111328125 ms
html object: 2967.419189453125 ms

3. 虚拟dom是如何转换为真实dom的?

在一个组件实例首次被渲染时

  1. 先生成虚拟dom树
  2. 然后根据虚拟dom树创建真实dom,并把真实dom挂载到页面中合适的位置。

此时,每个虚拟dom便会对应一个真实的dom。

例子:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div id="app">
      <h1>{{title}}</h1>
    </div>

    <script src="./vue.min.js"></script>
    <script>
      var div1 = document.getElementById('app');
      var vm = new Vue({
        // 组件配置
        el:"#app",
        data: {
          title: 'Hello Vue!!!',
        },
        template: '<ul><li>{{title}}</li></ul>',
        render(h) {
          // 目标,创建虚拟dom:这个组件到底要显示啥
          // h是一个函数,专门用于创建虚拟dom
          console.log('render');
          var vnode = h(
            'div',
            {
              // 虚拟dom的一些配置
              attrs: {
                id: 'app',
              },
            },
            // 子元素
            [h('h1', `标题:${this.title}`)]
          );
          console.log(vnode);
          return vnode;
        },
      });

      var div2 = document.getElementById('app');
      console.log(div1 === div2); 
      // false
    </script>
  </body>
</html>

虚拟dom有elm属性是标签


如果一个组件受响应式数据变化的影响,需要重新渲染时,它仍然会重新调用render函数,创建出一个新的虚拟dom树,用新树和旧树对比,通过对比,vue会找到最小更新量,然后更新必要的真实dom节点。

对比的是虚拟dom,改动的是真实dom。
这样一来,就保证了对真实dom达到最小的改动。

4. 模板和虚拟dom的关系

vue框架中有一个compile模块,它主要负责 将模板template转换(编译)为render函数,而render函数调用后将得到虚拟dom。

编译的过程分两步:

    1. 将模板字符串转换成为AST
    2. AST转换为render函数

运行时编译

如果使用传统的引入方式,则编译发生在组件第一次加载时,这称之为运行时编译。

比如直接引用vue.js

模板预编译

如果是在vue-cli的默认配置下,编译发生在打包时,这称之为模板预编译。

打包结果包含render函数,不再是template模板了


编译是一个极其耗费性能的操作,预编译可以有效的提高运行时的性能,而且,由于运行的时候已不需要编译,vue-cli在打包时会排除掉vue中的compile模块,以减少打包体积

要想打包的时候把编译器也打包进去,这样运行的时候也可以编译,需要添加配置文件vue.config.js 注意:预编译针对的是单文件组件的