虚拟DOM与Diff算法

参考

真实DOM的渲染

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

 

浏览器真实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本质上是一个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>

虚拟DOM的更新不会立即操作DOM,而是会通过diff算法,找出需要更新的节点,按需更新,并将更新的内容保存为一个js对象,更新完成后再挂载到真实dom上,实现真实的dom更新。

虚拟DOM优点(上面引出的)(这就是为啥需要虚拟DOM

  1. 小修改无需频繁更新DOM,框架的diff算法会自动比较,分析出需要更新的节点,按需更新

  2. 更新数据不会造成频繁的回流与重绘

  3. 表达力更强,数据更新更加方便

  4. 保存的是js对象,具备跨平台能力

不足

首次渲染大量DOM时,由于多了一层虚拟DOM的计算,会比innerHTML插入慢。

虚拟DOM实现原理

主要分三部分:

  1. 通过js建立节点描述对象

  2. diff算法比较分析新旧两个虚拟DOM差异

  3. 将差异patch到真实dom上实现更新

Diff算法

为了避免不必要的渲染,按需更新,虚拟DOM会采用Diff算法进行虚拟DOM节点比较,比较节点差异,从而确定需要更新的节点,再进行渲染。vue采用的是深度优先,同层比较的策略。

2e41e4f6bd68ca346c52492440e29540.png

新节点与旧节点的比较主要是围绕三件事来达到渲染目的

  1. 创建新节点

  2. 删除废节点

  3. 更新已有节点

Diff过程就是调用patch函数,比较新老节点,一边比较一边给真实DOM打补丁(patch);

 patch 函数代码:

//patch函数 oldVnode:老节点 vnode:新节点
function patch (oldVnode, vnode) {undefined
 ...
 if (sameVnode(oldVnode, vnode)) {undefined
 patchVnode(oldVnode, vnode) //如果新老节点是同一节点,那么进一步通过patchVnode来比较子节点
 } else {undefined
 /* -----否则新节点直接替换老节点----- */
 const oEl = oldVnode.el // 当前oldVnode对应的真实元素节点
 let parentEle = api.parentNode(oEl) // 父元素
 createEle(vnode) // 根据Vnode生成新元素
 if (parentEle !== null) {undefined
  api.insertBefore(parentEle, vnode.el, api.nextSibling(oEl)) // 将新元素添加进父元素
  api.removeChild(parentEle, oldVnode.el) // 移除以前的旧元素节点
  oldVnode = null
 }
 }
 ...
 return vnode
}
 
//判断两节点是否为同一节点
function sameVnode (a, b) {undefined
 return (
 a.key === b.key && // key值
 a.tag === b.tag && // 标签名
 a.isComment === b.isComment && // 是否为注释节点
 // 是否都定义了data,data包含一些具体信息,例如onclick , style
 isDef(a.data) === isDef(b.data) && 
 sameInputType(a, b) // 当标签是<input>的时候,type必须相同
 )
}

 

posted @ 2022-08-17 18:08  银河1992  阅读(64)  评论(0编辑  收藏  举报