Vue源码之虚拟DOM

40次阅读

共计 4119 个字符,预计需要花费 11 分钟才能阅读完成。

个人博客地址

什么是虚拟DOM

虚拟 DOM 是随着时代发展而诞生的产物。

在 web 早期,都是命令式的操作 DOM,虽然简单好用,但是不会维护。

现在,三大主流框架都是声明式的操作 DOM,通过描述状态和 DOM 之间的映射关系,来渲染成视图。状态怎么生成视图,不需要你来关心,框架会帮你搞定。

当某个状态发生改变时,如何只更新与这个状态相关联的 DOM 节点。

虚拟 DOM 的解决方式是:根据状态生成一个虚拟节点树,然后使用虚拟节点树进行渲染。在渲染前会将新的虚拟节点树和旧的虚拟节点树进行对比,只渲染不同的地方。

虚拟节点树是由组件树建立起来的整个虚拟节点(vnode)树。

vnode 是 JavaScript 中一个很普通的对象,这个对象上保存了生成 DOM 节点需要的一些数据。

为什么要引入虚拟 DOM

在 Angular 和 React 中,它们只知道有状态(state)变化了,但是不是到具体是哪个或者哪些状态(state)变化了,所以就需要进行比较暴力的对比,React 是通过虚拟 Dom 进行对比,Angular 是使用脏检查流程。

Vue 的变化侦测和这两个都不一样,Vue 在一定程度是知道哪些状态发生了变化。但是如果细粒度太细,每一个绑定都会有一个 watcher 实例来观察状态的变化,这样就会有一些内存开销以及一些依赖追踪的开销。当状态越多的节点被使用时,开销就越大。

因此在 Vue2 把细粒度调整到中,组件级别是一个 watcher 实例,不管组件内部使用了多少次,因此当状态变化时,只通知到组件,在组件内部使用虚拟 DOM 去进行对比更新。

Vue.js 中的虚拟 DOM

在 vue 中使用模板来描述状态和 DOM 之间的映射关系,根据模板生成 render 渲染函数,执行 render 渲染函数生成虚拟节点树,然后再与旧的虚拟节点树对比,最后渲染 DOM。

虚拟 DOM 在 vue 中主要做了两件事:

  • 提供与真实 DOM 相对应的 vnode
  • 把新的虚拟节点树和旧的虚拟节点树进行对比,更新不同的地方

对两个虚拟节点进行对比是整个虚拟 DOM 中最核心的算法,它可以判断出那个节点发生了变更,从而只更新变更的节点。

vnode

Vue 中有一个 VNode 类,可以实例化不同类型的 vnode,不同类型的 vnode 表示不同类型的 DOM 元素:

class VNode {
  tag: string | void;
  data: VNodeData | void;
  children: ?Array<VNode>;
  text: string | void;
  elm: Node | void;
  ns: string | void;
  context: Component | void; // rendered in this component's scope
  key: string | number | void;
  componentOptions: VNodeComponentOptions | void;
  componentInstance: Component | void; // component instance
  parent: VNode | void; // component placeholder node

  // strictly internal
  raw: boolean; // contains raw HTML? (server only)
  isStatic: boolean; // hoisted static node
  isRootInsert: boolean; // necessary for enter transition check
  isComment: boolean; // empty comment placeholder?
  isCloned: boolean; // is a cloned node?
  isOnce: boolean; // is a v-once node?
  asyncFactory: Function | void; // async component factory function
  asyncMeta: Object | void;
  isAsyncPlaceholder: boolean;
  ssrContext: Object | void;
  fnContext: Component | void; // real context vm for functional nodes
  fnOptions: ?ComponentOptions; // for SSR caching
  devtoolsMeta: ?Object; // used to store functional render context for devtools
  fnScopeId: ?string; // functional scope id support

  constructor (
    tag?: string,
    data?: VNodeData,
    children?: ?Array<VNode>,
    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
  }
}

简单的说,vnode 可以理解为 节点描述对象 ,描述了怎么去创建一个真实的DOMDOM 元素上的所有属性堵在 vnode 上有对应的属性。

vnode 表示一个真正的 DOM 元素所有真实的 DOM 元素都使用 vnode 创建,并插入到视图中去。

vnode 的作用

每次渲染视图时,都会创建新的 vnode,与旧的 vnode 进行对比,找出不一样的地方并更新。

Vue 目前采用的是中等密度的细粒度,当状态变化时,只会通知到组件,在组件内部使用虚拟 DOM 来渲染视图。

也就是说只要组件内部有一个状态发生了改变了,整个组件都会重新渲染。如果组件只变化了一个节点,却要重新渲染所有的节点,这会造成性能浪费。

因此对比新旧 vnode,只更新不同的部分,就显得尤为重要。

VNode 的类型

  • 注释节点

    export const createEmptyVNode = (text: string = '') => {const node = new VNode()
      node.text = text
      node.isComment = true
      return node
    }
  • 文本节点

    export function createTextVNode (val: string | number) {return new VNode(undefined, undefined, undefined, String(val))
    }
  • 元素节点

    元素节点通常包括一下 4 种属性:

    • tag 节点名称,例如:div
    • data 节点数据,例如:class style
    • children 当前节点的子节点列表
    • context 当前组件的 Vue 实例
  • 组件节点

    和元素节点差不多,有两个特有属性:

    • componentOptions 组件节点的选项参数,包括:propsData、tag、children
    • componentInstance 组件实例。在 Vue 中每个组件都是一个 Vue 实例。
  • 函数式节点

    和组件节点类似,有两个特有属性:

    • functionalContext
    • functionalOptions
  • 克隆节点

    克隆节点是将现有节点的所有属性都复制到新节点下面,直接复用现有的 vnode,出了首次渲染外,后续更新都不需要执行渲染函数生成相同的 vnode,从而提升一定程度的性能。

    export function cloneVNode (vnode: VNode): VNode {
      const cloned = new VNode(
        vnode.tag,
        vnode.data,
        // #7975
        // clone children array to avoid mutating original in case of cloning
        // a child.
        vnode.children && vnode.children.slice(),
        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
    }

总结:

Vue 中,VNode 是一个类,不同类型的 VNode 实例代表不同类型的元素节点。

Vue2 对状态的侦测策略采用了中等粒度,当状态发生变化时,只会通知到组件,组件内部再使用虚拟 DOM 进行更新。组件内不是所有的节点都需要更新,所以将渲染函数新生成的 vnode 和旧的 vnode 进行对比,只更新不同的部分。

下一篇会介绍虚拟 DOM 最核心的算法:patch。

正文完
 0