最近复习到虚拟DOM与Diff,翻阅了众多资料,特此总结了这篇长文,加深自己对vue的理解。这篇文章比较详细的分析了vue的虚拟DOM,Diff算法,其中一些关键的地方从别处搬运了一些图进行说明(感谢制图的大佬),也包含比较详细的源码解读。

真实DOM的渲染

在讲虚拟DOM之前,先说一下真实DOM的渲染。

https://upload-images.jianshu.io/upload_images/15050783-c14e5c2c54996b48.png?imageMogr2/auto-orient/strip|imageView2/2/w/646/format/webp

浏览器真实DOM渲染的过程大概分为以下几个部分

  1. 构建DOM树。通过HTML parser解析处理HTML标记,将它们构建为DOM树(DOM tree),当解析器遇到非阻塞资源(图片,css),会继续解析,但是如果遇到script标签(特别是没有async 和 defer属性),会阻塞渲染并停止html的解析,这就是为啥最好把script标签放在body下面的原因。
  2. 构建CSSOM树。与构建DOM类似,浏览器也会将样式规则,构建成CSSOM。浏览器会遍历CSS中的规则集,根据css选择器创建具有父子,兄弟等关系的节点树。
  3. 构建Render树。这一步将DOM和CSSOM关联,确定每个 DOM 元素应该应用什么 CSS 规则。将所有相关样式匹配到DOM树中的每个可见节点,并根据CSS级联确定每个节点的计算样式,不可见节点(head,属性包括 display:none的节点)不会生成到Render树中。
  4. 布局/回流(Layout/Reflow)。浏览器第一次确定节点的位置以及大小叫布局,如果后续节点位置以及大小发生变化,这一步触发布局调整,也就是 回流
  5. 绘制/重绘(Paint/Repaint)。将元素的每个可视部分绘制到屏幕上,包括文本、颜色、边框、阴影和替换的元素(如按钮和图像)。如果文本、颜色、边框、阴影等这些元素发生变化时,会触发重绘(Repaint)。为了确保重绘的速度比初始绘制的速度更快,屏幕上的绘图通常被分解成数层。将内容提升到GPU层(可以通过tranform,filter,will-change,opacity触发)可以提高绘制以及重绘的性能。
  6. 合成(Compositing)。这一步将绘制过程中的分层合并,确保它们以正确的顺序绘制到屏幕上显示正确的内容。

为啥需要虚拟DOM

上面这是一次DOM渲染的过程,如果dom更新,那么dom需要重新渲染一次,如果存在下面这种情况

<body>
    <div id="container">
        <div class="content" style="color: red;font-size:16px;">
            This is a container
        </div>
				....
        <div class="content" style="color: red;font-size:16px;">
            This is a container
        </div>
    </div>
</body>
<script>
    let content = document.getElementsByClassName('content');
    for (let i = 0; i < 1000000; i++) {
        content[i].innerHTML = `This is a content${i}`;
        // 触发回流
        content[i].style.fontSize = `20px`;
    }
</script>

那么需要真实的操作DOM100w次,触发了回流100w次。每次DOM的更新都会按照流程进行无差别的真实dom的更新。所以造成了很大的性能浪费。如果循环里面是复杂的操作,频繁触发回流与重绘,那么就很容易就影响性能,造成卡顿。另外这里要说明一下的是,虚拟DOM并不是意味着比DOM就更快,性能需要分场景,虚拟DOM的性能跟模板大小是正相关。虚拟DOM的比较过程是不会区分数据量大小的,在组件内部只有少量动态节点时,虚拟DOM依然是会对整个vdom进行遍历,相比直接渲染而言是多了一层操作的。

	<div class="list">
    <p class="item">item</p>
    <p class="item">item</p>
    <p class="item">item</p>
    <p class="item">{{ item }}</p>
    <p class="item">item</p>
    <p class="item">item</p>
  </div>

比如上面这个例子,虚拟DOM。虽然只有一个动态节点,但是虚拟DOM依然需要遍历diff整个list的class,文本,标签等信息,最后依然需要进行DOM渲染。如果只是dom操作,就只要操作一个具体的DOM然后进行渲染。虚拟DOM最核心的价值在于,它能通过js描述真实DOM,表达力更强,通过声明式的语言操作,为开发者提供了更加方便快捷开发体验,而且在没有手动优化,大部分情景下,保证了性能下限,性价比更高。

虚拟DOM

虚拟DOM本质上是一个js对象,通过对象来表示真实的DOM结构。tag用来描述标签,props用来描述属性,children用来表示嵌套的层级关系。

const vnode = {
    tag: 'div',
    props: {
        id: 'container',
    },
    children: [{
        tag: 'div',
        props: {
            class: 'content',
        },
      	text: 'This is a container'
    }]
}

//对应的真实DOM结构
<div id="container">
  <div class="content">
    This is a container
  </div>
</div>