虚拟 DOM 的实现原理

理解虚拟 DOM 的概念、作用及其在 Vue 中的实现方式

问题

什么是虚拟 DOM?如何实现一个虚拟 DOM?

解答

什么是虚拟 DOM

虚拟 DOM 是对真实 DOM 的抽象,用 JavaScript 对象(VNode 节点)来描述 DOM 树结构。一个虚拟 DOM 对象至少包含三个属性:标签名(tag)、属性(attrs)和子元素(children)。

以 Vue 为例,真实 DOM:

<div id="app">
    <p class="p">节点内容</p>
    <h3>{{ foo }}</h3>
</div>

对应的虚拟 DOM:

const app = new Vue({
    el: "#app",
    data: {
        foo: "foo"
    }
})

// render 函数生成的虚拟 DOM
(function anonymous() {
    with(this) {
        return _c('div', {attrs: {"id": "app"}}, [
            _c('p', {staticClass: "p"}, [_v("节点内容")]),
            _v(" "),
            _c('h3', [_v(_s(foo))])
        ])
    }
})

为什么需要虚拟 DOM

DOM 操作的性能开销很大。传统方式每次更新都会触发完整的渲染流程,如果连续更新 10 个 DOM 节点,浏览器会执行 10 次完整流程。

虚拟 DOM 的优势:

  1. 减少 DOM 操作:将多次更新合并,通过 diff 算法计算最小变更,一次性更新到真实 DOM
  2. 跨平台能力:抽象了渲染过程,可以渲染到不同平台(浏览器、Native、小程序等)

Vue 中的 VNode 结构

export default class VNode {
  tag: string | void;              // 标签名
  data: VNodeData | void;          // 节点数据
  children: ?Array<VNode>;         // 子节点
  text: string | void;             // 文本内容
  elm: Node | void;                // 对应的真实 DOM 节点
  context: Component | void;       // Vue 实例
  key: string | number | void;    // 节点的 key
  componentOptions: VNodeComponentOptions | void;
  componentInstance: Component | void;
  parent: VNode | void;
  isStatic: boolean;               // 是否静态节点
  isComment: boolean;              // 是否注释节点
  // ...
}

创建 VNode 的过程

Vue 通过 createElement 方法创建 VNode:

export function createElement(
  context: Component,
  tag: any,
  data: any,
  children: any,
  normalizationType: any,
  alwaysNormalize: boolean
): VNode | Array<VNode> {
  // 参数处理
  if (Array.isArray(data) || isPrimitive(data)) {
    normalizationType = children
    children = data
    data = undefined
  }
  if (isTrue(alwaysNormalize)) {
    normalizationType = ALWAYS_NORMALIZE
  }
  return _createElement(context, tag, data, children, normalizationType)
}

核心实现在 _createElement 方法中:

export function _createElement(
    context: Component,
    tag?: string | Class<Component> | Function | Object,
    data?: VNodeData,
    children?: any,
    normalizationType?: number
): VNode | Array<VNode> {
    // 规范化 children
    if (normalizationType === ALWAYS_NORMALIZE) {
        children = normalizeChildren(children)
    } else if (normalizationType === SIMPLE_NORMALIZE) {
        children = simpleNormalizeChildren(children)
    }
    
    let vnode, ns
    if (typeof tag === 'string') {
        let Ctor
        ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
        
        if (config.isReservedTag(tag)) {
            // 内置标签,创建普通 VNode
            vnode = new VNode(
                config.parsePlatformTagName(tag), data, children,
                undefined, undefined, context
            )
        } else if (isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
            // 组件,通过 createComponent 创建
            vnode = createComponent(Ctor, data, context, children, tag)
        } else {
            vnode = new VNode(tag, data, children, undefined, undefined, context)
        }
    }
    
    return vnode
}

对于组件类型,使用 createComponent 创建 VNode:

export function createComponent(
  Ctor: Class<Component> | Function | Object | void,
  data: ?VNodeData,
  context: Component,
  children: ?Array<VNode>,
  tag?: string
): VNode | Array<VNode> | void {
  if (isUndef(Ctor)) {
    return
  }
  
  // 构建子类构造函数
  const baseCtor = context.$options._base
  
  if (isObject(Ctor)) {
    Ctor = baseCtor.extend(Ctor)
  }
  
  // 安装组件钩子函数
  installComponentHooks(data)
  
  // 实例化 vnode
  const vnode = new VNode(
    `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
    data, undefined, undefined, undefined, context,
    { Ctor, propsData, listeners, tag, children },
    asyncFactory
  )
  
  return vnode
}

关键点

  • 虚拟 DOM 是用 JavaScript 对象描述 DOM 树结构,包含 tag、data、children 等属性
  • 通过 diff 算法计算最小变更,减少真实 DOM 操作次数,提升性能
  • 抽象了渲染过程,实现跨平台能力(Web、Native、小程序等)
  • Vue 通过 createElement 创建 VNode,会对 children 进行规范化处理
  • 每个 VNode 的 children 也是 VNode,形成树形结构映射真实 DOM 树