虚拟 DOM 的实现原理
trycoding.fun
Vue

虚拟 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 树