vlambda博客
学习文章列表

虚拟 dom 是如何提升 vue 的渲染效率的(虚拟 dom 如何提升性能)?

虚拟DOM

1,dom 渲染原理

2,虚拟 dom 是什么?

3,虚拟 dom 在 vue 中做了什么?

4,虚拟 dom 是如何提升 vue 的渲染效率的(虚拟 dom 如何提升性能)?


1,dom渲染原理

以webkit为例,dom渲染的过程是怎样的呢?先来看张图:

大概分为以下几个步骤:

1,浏览器先下载html文件,html parser(解析器)会将其解析为 dom 树
2,下载css文件,css parser会将其解析为 cssom 树
3,dom和cssom合成为render tree(呈现树)
4,布局/回流(layout/reflow)
5,绘制(paint)

dom 树:

虚拟 dom 是如何提升 vue 的渲染效率的(虚拟 dom 如何提升性能)?

Cssom (css object model)树:

虚拟 dom 是如何提升 vue 的渲染效率的(虚拟 dom 如何提升性能)?

render树(呈现树/渲染树):

dom树是骨架,骨架构建完后就要读取css文件,每个dom元素会去cssom中查询自己的修饰信息,这就相当于合并dom和cssom的动作。而这个动作完成后,输出来的就是呈现树了。呈现树的长相跟dom树不太一样,突出的是呈现二字,也就是一定要是肉眼看得到的东西。比如标签就不会出现在呈现树中,因为不可见,同样的还有display:none的元素,呈现树也不会构建。

呈现树主要是根据每个dom元素的display信息,构建成一个个的方块。比如display:block的元素,会构建成一个大方块,而display:inline的元素,就构建成一个小方块。

换句话说,呈现树是由多个带有视觉属性(比如颜色和尺寸)的方块组成。

呈现树的节点和dom树并不是一一对应的。比如select元素,dom树只有一个,而呈现树则会构建出三个:一个是显示区域,一个是下拉列表框,还有一个用于按钮

虚拟 dom 是如何提升 vue 的渲染效率的(虚拟 dom 如何提升性能)?
布局、回流:

构建完呈现树后,我们需要根据每个元素的位置将其都摆放好,也就是为每个节点分配一个应出现在屏幕上的确切坐标,这就是layout布局。

这里多啰嗦几句,玩过PS的同学都知道psd图,在设计psd时,都是先一个一个小图单独分层绘制,然后根据客户的要求统一排版,比如这个图往左边挪一下,那个图靠右边点这样。呈现树也类似,先把草稿绘制好,最后根据css规定的位置(客户要求)统一进行排版,这就是布局。

当呈现树发生了变化时(比如js操作dom让某元素的位置发生了改变),那就得重新布局,这就是reflow(回流)。

绘制:

布局动作完成后,GUI渲染线程会遍历呈现树,将每个节点绘制出来,这就是paint绘制。可以想象成喷墨打印机开始打印了

2,虚拟 dom 是什么?
  • vue2.x才有虚拟dom的
  • 虚拟 dom 本质就是一个js对象,所以具有跨平台的特质
3,虚拟 dom 在 vue 中做了什么?

首先先看下vue的渲染过程:

虚拟 dom 是如何提升 vue 的渲染效率的(虚拟 dom 如何提升性能)?
  • 第一步:写一个div元素
  • 第二步:vue中有一个render()函数,render()负责将 template 模板里的东西转化为真实的 dom,比如会根据 template 中的 div 元素,通过 createElement 创建一个真实的 div,这时候就会生成对应的虚拟 dom
  • 第三步:会根据创建好的真实 dom 生成虚拟 dom 也就是 js 对象,最后再变成真实 dom, 然后浏览器最终呈现

所以虚拟 dom 在 vue 中做了:

  • 1,将真实 dom 转为 虚拟 dom (js 对象)
  • 2,更新的时候做对比
4,虚拟 dom 是如何提升 vue 的渲染效率的(虚拟 dom 如何提升性能)?

上文dom渲染原理中也有提到,当浏览器拿到 dom 和 css 的骨架结构后,接着就是将 dom 树的各元素根据尺寸和位置进行布局排列,这就是layout。然后对各元素进行上色,这就是paint。

以上过程是必不可少的,其实我们关注的重点是:一旦浏览器解析 dom 完毕,如何降低 dom 的更新次数?

触发重新布局的话就是reflow(回流),重新上色的话就是paint(绘制)。

<ul>
  <li>1</li>
  <li>2</li>
</ul>

浏览器解析完毕后,如果对第二个 li 进行 display: none、display: block的话,那就是触发了两次reflow和repaint(重新画)

脱离文档流

对脱离文档流的元素进行操作,就可以有效减少文档流本身的重排。我们熟知的css脱离文档流的方式主要是float浮动与position定位,将频繁重排的元素,position属性设为absolute或fixed是个不错的选择,因为元素脱离了文档流,它的变化不会影响到其他元素。

还有一个比较常用的,就是文档碎片DocumentFragment

比如我们要给ul添加5个li节点,二者的区别是:

直接操作DOM,需要重排5次:

使用DocumentFragment一次性添加,只需重排1次:

2

为检验结论是否正确,我们写段测试代码,分别对比下渲染完成的时间:

<body>
  <ul class="list1"></ul>
  <ul class="list2"></ul>
</body>
</html>
<script>
  // 直接操作 DOM 添加节点
  console.time('time1')
  let list1 = document.querySelector('.list1'),
      i = 50000
  while (i--) {
    list1.appendChild(document.createElement('li'))
  }
  console.timeEnd('time1')

  // 使用 documentFragement 添加节点
  console.time('time2')
  let list2 = document.querySelector('.list2'),
      fragment = document.createDocumentFragment(),
      n = 50000
  while(n--) {
    fragment.appendChild(document.createElement('li'))
  }
  list2.appendChild(fragment)
  console.timeEnd('time2')
</script>

运行结果:

time1:26 毫秒

time:24 毫秒

PS:

这段代码很有意思,如果在谷歌浏览器测试,结果反而是直接操作dom比较快,这是因为谷歌本身做了优化设计,会将短时间内的多次reflow收集起来组成队列,在一定时间后flush队列,将多个reflow的变为一次reflow,可以多跑几款浏览器看看

虚拟dom:

以vue为例,虚拟 dom 之所以能提升性能,核心思想就是将真实 dom 映射为 js 对象,通过捕获收集object的差异,将对dom的操作一次性压入DocumentFragment,等待下一次事件循环再进行reflow,这样就有效减少了dom的操作次数,提升了渲染效率。

vue的两大核心:组件化、数据驱动

  • 1,局部更新(节点数据)
  • 2,将直接操作 dom 的地方拿到两个 js 对象之中去进行比较