Vue中的虚拟DOM和Diff算法

发布时间 2023-07-26 09:23:51作者: leayun

一、 虚拟DOM

1. 什么是虚拟DOM?

一个用来表示真实 DOM 节点 的 JS 对象,主要包含标签名 tag、属性 attrs 和子元素对象 children 属性等。

代码示例如下:

<div class="contain" id="baseNo">
  <h4 class="item">标题</h4>
  <p class="item">段落内容</p>
</div>

{
  tag:'div',
  attrs:{
    id:'baseNo',
    class:'contain'
  },
  children:[
     {
      tag:'h4',
      attrs:{
        class:'item'
      },
      text:'标题'
    }
    {
      tag:'p',
      attrs:{
        class:'item'
      },
      text:'段落内容'
    },
  ]

}

2. 虚拟 DOM 存在的意义

大家都知道真实 DOM 是一棵节点树,在渲染时需要很大的性能开销,如果每次数据发生变化时,都直接渲染到真实 DOM 上会引起整个 DOM 树的重绘和重排,而通过虚拟 DOM-Diff 算法只需要在数据发生变化时,对新旧虚拟 DOM 进行差异化的比较,然后对真实 DOM 进行局部的更新操作,大大较少了渲染性能的开销,从而提高页面的渲染性能和用户体验。

3. 虚拟 DOM 具体实现

前面我们介绍了虚拟 DOM 的概念以及虚拟 DOM 存在的意义,那么在 Vue 中虚拟 DOM 是怎么实现的呢?接下来,我们从源码出发,深入学习一下

3.1 VNode 类

虚拟 DOM 是用 JS 对象来描述一个真实的 DOM 节点。而在 Vue 中就存在了一个 VNode 类,VNode 类中包含了描述一个真实 DOM 节点所需要的一系列属性,如 tag 表示节点的标签名,text 表示节点中包含的文本,children 表示该节点包含的子节点等。源码如下:

export default class VNode {
  tag?: string/*当前节点的标签名*/
  data: VNodeData | undefined/*当前节点对应的数据对象,包含了具体的一些数据信息,
  是一个VNodeData类型,可以参考VNodeData类型中的数据信息*/
  children?: Array<VNode> | null/*当前节点的子节点集合,是一个数组*/
  text?: string /*当前节点的文本*/
  elm: Node | undefined/*当前虚拟节点对应的真实dom节点*/
  ns?: string/*当前节点的名字空间*/
  context?: Component /*当前组件节点对应的Vue实例*/
  key: string | number | undefined/*节点的key属性,被当作节点的标志,用以优化*/
  componentOptions?: VNodeComponentOptions/*组件的option选项*/
  componentInstance?: Component /*当前节点对应的组件的实例*/
  parent: VNode | undefined | null /*当前节点的父节点*/

  // strictly internal
  raw: boolean /*简而言之就是是否为原生HTML或只是普通文本,
  innerHTML的时候为true,textContent的时候为false*/
  isStatic: boolean /*静态节点标志*/
  isRootInsert: boolean /*是否作为根节点插入*/
  isComment: boolean /*是否为注释节点*/
  isCloned: boolean  /*是否为克隆节点*/
  isOnce: boolean /*是否有v-once指令*/
  asyncFactory?: Function // async component factory function
  asyncMeta: Object | void
  isAsyncPlaceholder: boolean
  ssrContext?: Object | void
  fnContext: Component | void // /*函数式组件对应的Vue实例*/
  fnOptions?: ComponentOptions | null // for SSR caching
  devtoolsMeta?: Object | null // used to store functional render context for devtools
  fnScopeId?: string | null // functional scope id support
  isComponentRootElement?: boolean | null // for SSR directives

  constructor(
    tag?: string,
    data?: VNodeData,
    children?: Array<VNode> | null,
    text?: string,
    elm?: Node,
    context?: Component,
    componentOptions?: VNodeComponentOptions,
    asyncFactory?: Function
  ) {
    this.tag = tag
    this.data = data
    this.children = children
    this.text = text
    this.elm = elm
    this.ns = undefined
    this.context = context
    this.fnContext = undefined
    this.fnOptions = undefined
    this.fnScopeId = undefined
    this.key = data && data.key
    this.componentOptions = componentOptions
    this.componentInstance = undefined
    this.parent = undefined
    this.raw = false
    this.isStatic = false
    this.isRootInsert = true
    this.isComment = false
    this.isCloned = false
    this.isOnce = false
    this.asyncFactory = asyncFactory
    this.asyncMeta = undefined
    this.isAsyncPlaceholder = false
  }

  get child(): Component | void {
    return this.componentInstance
  }
}

通过属性之间不同的搭配,可以实例化出不同类型的真实 DOM 节点。

3.2 VNode 的类型

接下来我们通过阅读源码,对不同属性搭配出的以下几种类型节点进行分析

  1)注释节点

  2)文本节点

  3)克隆节点

3.2.1 注释节点

注释节点主要包含 text 和 isComment 俩个属性,其中 text 属性表示具体的注释信息, isComment 是一个标志,用来表示一个节点是否是注释节点。源码如下:

// 创建注释节点
export const createEmptyVNode = (text: string = '') => {
  const node = new VNode()
  node.text = text
  node.isComment = true
  return node
}
3.2.2 文本节点

文本节点只需要一个属性,那就是 text 属性,主要用来表示具体的文本内容。源码如下:

// 创建文本节点
export function createTextVNode (val: string | number) {
  return new VNode(undefined, undefined, undefined, String(val))
}
3.2.3 克隆节点

克隆节点就是把现有节点的属性全部复制到新节点中,而现有节点和新克隆得到的节点之间的不同在于克隆得到的节点中的 isCloned 值为 true,源码如下:

// 创建克隆节点
export function cloneVNode(vnode: VNode): VNode {
  const cloned = new VNode(
    vnode.tag,
    vnode.data,
    vnode.children,
    vnode.text,
    vnode.elm,
    vnode.context,
    vnode.componentOptions,
    vnode.asyncFactory
  )
  cloned.ns = vnode.ns
  cloned.isStatic = vnode.isStatic
  cloned.key = vnode.key
  cloned.isComment = vnode.isComment
  cloned.fnContext = vnode.fnContext
  cloned.fnOptions = vnode.fnOptions
  cloned.fnScopeId = vnode.fnScopeId
  cloned.asyncMeta = vnode.asyncMeta
  cloned.isCloned = true
  return cloned
}

以上几种节点类型它们本质上都是 VNode 类的实例,只是在实例化的时候传入的属性参数不同而已。

二、Diff 算法

1. 什么是 Diff 算法?

Diff 算法是一种高效的算法,可以在不重建所有节点的情况下,将新旧虚拟 DOM 比较,找出需要更新的节点,从而实现局部更新,提高应用程序的性能。Diff算法还约定只做同层级节点比对,而不是跨层级节点比对,即深度优先遍历算法,在同级之间则运用首尾指针法进行比较。

Diff 算法的作用:本质就是比较俩个 JS 对象的差异。

2. Diff 算法的实现原理

当数据修改后会触发 setter 劫持操作,我们在 setter 中执行 dep.notify(),通知所有的订阅者 watcher 重新渲染。 订阅者 watcher 这时会在回调内部,通过 vm._render()获取最新的虚拟 DOM;然后通过 patch 方法比对新旧虚拟 DOM,给真实 DOM 元素进行打补丁,更新视图的操作。 下面通过视图进行解析:

2.1 数据发生变化时,节点更新视图解析

image

2.2 节点进行比较时,同层级节点进行比对

image

2.3 updateChildren时, 首尾指针法视图解析

规则:

  1)依次比较,当比较成功后退出当前比较

  2)渲染结果以 newVnode 为准

  3)每次比较成功后 start 点和 end 点向中间靠拢

  4)当新旧节点中有一个 start 点跑到 end 点右侧时终止比较

  5)如果都匹配不到,则旧虚拟 DOM kev 值去比对 新虚拟 DOM 的 key 值,如果 key 相同则复用,并移动到新虚拟 DOM 的位置。

初始化:

  旧虚拟节点(oldVnode):首指针(oldSart),尾指针(oldEnd)

  新虚拟节点(newVnode):首指针(newSart),尾指针(newEnd)
image
第一步:首先,oldSart和newSart做比对,如果没有比对成功,就用oldSart和newEnd进行比对,当比对成功,就退出比对,将真实DOM中当前元素移至与新虚拟节点相同位置,并将oldS和newE指针向中间靠拢
imageimageimage

第二步:重复第一步操作,当oldVnode和newVnode其中有一个首指针跑到尾指针的右侧就终止比较,如oldVnode中有未匹配的元素,将其卸载;newVnode中有未匹配的,在真实DOM中相同位置新增即可。
imageimageimage
imageimageimage

三、总结

使用虚拟 DOM+Diff算法,主要是通过 JS 计算去替代对真实 DOM 的部分操作,最小化的更新视图,从而提高页面的渲染性能和用户体验。