Vue 源码分析 —— 创建虚拟 DOM

883 阅读4分钟

虚拟 DOM 的创建

什么是虚拟 DOM

Virtual DOM 即虚拟 DOM 节点。是通过 JS 对象来模拟 DOM 中节点,然后通过特定的 render 方法将虚拟 DOM 渲染成真实的 DOM 节点

为什么使用虚拟 DOM

虚拟 DOM 是为了解决频繁的操作 DOM 元素引发性能问题的产物。当使用 JS 脚本操作 DOM 元素时,会引发浏览器的回流或者重绘。我们来聊几一下回流和重绘的概念:

  • 回流: 当我们对 DOM 元素的修改引发元素尺寸的变化时,浏览器需要重新计算元素的大小和位置,最后将重新计算的结果绘制到屏幕上,这个过程被称为回流

  • 重绘: 当我们对 DOM 元素的修改值只改变元素的颜色时,浏览器此时并不需要重新计算元素的大小和位置,而只要重新绘制新样式。这个过程被称为重绘

很显然,回流会比重绘更加耗费性能。在使用虚拟 DOM 时,我们对 DOM 的操作,首先会操作在虚拟 DOM 上,虚拟 DOM 会将多个改动合并成一个批量的操作,从而减少 DOM 重排的次数,进而缩短生成渲染树和绘制的所花的时间。

Vue 中的虚拟 DOM

Vue 中,使用了 VNode 这样一个构造函数描述一个 DOM 节点。

VNode 构造函数

  /**
   * Vnode 构造函数
   * @param {*} tag 
   * @param {*} data 
   * @param {*} children 
   * @param {*} text 
   * @param {*} elm 
   * @param {*} context 
   * @param {*} componentOptions 
   * @param {*} asyncFactory 
   */
  var VNode = function VNode (
    tag,
    data,
    children,
    text,
    elm,
    context,
    componentOptions,
    asyncFactory
  ) {
    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;
  };

Vue 通过 VNode 这个构造函数来描述 DOM 节点,下面简单介绍一下注释节点和文本节点的创建方法

创建注释节点

/**
 * 创建注释节点(空节点)
 * @param {*} text 
 * @returns 
 */
export const createEmptyVNode = (text: string = '') => {
  const node = new VNode()
  node.text = text
  node.isComment = true
  return node
}

创建文本节点

/**
 * 创建文本节点
 * @param {*} val 
 * @returns 
 */
export function createTextVNode (val: string | number) {
  return new VNode(undefined, undefined, undefined, String(val))
}

虚拟 DOM 的创建

Vue 的挂载流程中,在获取到 render 函数之后,调用 vm._render 方法,将 render 函数转换成虚拟 DOM ,来看一下 vm._render 方法的实现

// /core/instance/render.js
// 将 Vue 实例转换成虚拟 DOM 
Vue.prototype._render = function (): VNode {
  const vm: Component = this
  const { render, _parentVnode } = vm.$options

  if (_parentVnode) {
    vm.$scopedSlots = normalizeScopedSlots(
      _parentVnode.data.scopedSlots,
      vm.$slots,
      vm.$scopedSlots
    )
  }

  // set parent vnode. this allows render functions to have access
  // to the data on the placeholder node.
  vm.$vnode = _parentVnode
  // render self
  let vnode
  try {
    // There's no need to maintain a stack becaues all render fns are called
    // separately from one another. Nested component's render fns are called
    // when parent component is patched.
    currentRenderingInstance = vm
    // 调用 render 函数创建生成虚拟 DOM,将 $createElement 方法作为 render 函数的第一个参数,这与手写 render 函数相同
    vnode = render.call(vm._renderProxy, vm.$createElement)
  } catch (e) {
    handleError(e, vm, `render`)
    // return error render result,
    // or previous vnode to prevent render error causing blank component
    /* istanbul ignore else */
    if (process.env.NODE_ENV !== 'production' && vm.$options.renderError) {
      try {
        vnode = vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e)
      } catch (e) {
        handleError(e, vm, `renderError`)
        vnode = vm._vnode
      }
    } else {
      vnode = vm._vnode
    }
  } finally {
    currentRenderingInstance = null
  }
  // if the returned array contains only a single node, allow it
  if (Array.isArray(vnode) && vnode.length === 1) {
    vnode = vnode[0]
  }
  // return empty vnode in case the render function errored out
  if (!(vnode instanceof VNode)) {
    if (process.env.NODE_ENV !== 'production' && Array.isArray(vnode)) {
      warn(
        'Multiple root nodes returned from render function. Render function ' +
        'should return a single root node.',
        vm
      )
    }
    vnode = createEmptyVNode()
  }
  // set parent
  vnode.parent = _parentVnode
  return vnode
}

可以看到,在 _render 方法中,最核心就是 vnode = render.call(vm._renderProxy, vm.$createElement),这时候,会将 render 函数转换成虚拟 DOM 。我们会想一下,在我们手写 render 函数时的过程,来看下面这个例子

Vue.component('anchored-heading', {
  render: function (createElement) {
    return createElement(
      'h' + this.level,   // 标签名称
      this.$slots.default // 子节点数组
    )
  },
  props: {
    level: {
      type: Number,
      required: true
    }
  }
})

在手写 render 函数时, render 函数接受一个函数作为参数,这个参数其实就是 vm.$createElement$createElement 其实是对 createElement 方法的封装, 而 createElement 又是对 _createElement 的封装,来看下这两个函数的具体实现:

// wrapper function for providing a more flexible interface
// without getting yelled at by flow
export function createElement (
  context: Component,
  tag: any,
  data: any,
  children: any,
  normalizationType: any,
  alwaysNormalize: boolean
): VNode | Array<VNode> {
  /**
   * 判断是否传递了 data 参数
   * 这里判断是否传递了 data 参数的标准是第三个参数是一个 object 类型(data 选项一般为 对象)
   */
  if (Array.isArray(data) || isPrimitive(data)) {
    normalizationType = children
    children = data
    data = undefined
  }
  // 区分手写的 render 方法和内部 template 编译得到的 template 方法
  if (isTrue(alwaysNormalize)) {
    normalizationType = ALWAYS_NORMALIZE
  }
  return _createElement(context, tag, data, children, normalizationType)
}

export function _createElement (
  context: Component,
  tag?: string | Class<Component> | Function | Object,
  data?: VNodeData,
  children?: any,
  normalizationType?: number
): VNode | Array<VNode> {
  // data 选项中的属性不能使用响应式对象
  if (isDef(data) && isDef((data: any).__ob__)) {
    process.env.NODE_ENV !== 'production' && warn(
      `Avoid using observed data object as vnode data: ${JSON.stringify(data)}\n` +
      'Always create fresh vnode data objects in each render!',
      context
    )
    return createEmptyVNode()
  }
  // object syntax in v-bind
  if (isDef(data) && isDef(data.is)) {
    tag = data.is
  }
  if (!tag) {
    // in case of component :is set to falsy value
    // 防止动态组件中 :is 属性的值为 false 时,需要做特殊处理,作为一个空节点进行返回
    return createEmptyVNode()
  }
  // warn against non-primitive key
  if (process.env.NODE_ENV !== 'production' &&
    isDef(data) && isDef(data.key) && !isPrimitive(data.key)
  ) {
    if (!__WEEX__ || !('@binding' in data.key)) {
      warn(
        'Avoid using non-primitive value as key, ' +
        'use string/number value instead.',
        context
      )
    }
  }
  // support single function children as default scoped slot
  if (Array.isArray(children) &&
    typeof children[0] === 'function'
  ) {
    data = data || {}
    data.scopedSlots = { default: children[0] }
    children.length = 0
  }
  if (normalizationType === ALWAYS_NORMALIZE) {
    // 手写 render 
    children = normalizeChildren(children)
  } else if (normalizationType === SIMPLE_NORMALIZE) {
    // template 编译得到的 render 函数
    children = simpleNormalizeChildren(children)
  }
  let vnode, ns
  if (typeof tag === 'string') {
    let Ctor
    ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
    // 判断是否为内置标签,例如 浏览器中的 html 标签
    if (config.isReservedTag(tag)) {

      // platform built-in elements
      // 内置标签,直接创建虚拟 DOM
      vnode = new VNode(
        config.parsePlatformTagName(tag), data, children,
        undefined, undefined, context
      )
    } else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
      // component
      // 已经注册的自定义组件,创建组件类型的 VNode
      vnode = createComponent(Ctor, data, context, children, tag)
    } else {
      // unknown or unlisted namespaced elements
      // check at runtime because it may get assigned a namespace when its
      // parent normalizes children
      vnode = new VNode(
        tag, data, children,
        undefined, undefined, context
      )
    }
  } else {
    // direct component options / constructor
    vnode = createComponent(tag, data, context, children)
  }
  if (Array.isArray(vnode)) {
    return vnode
  } else if (isDef(vnode)) {
    if (isDef(ns)) applyNS(vnode, ns)
    if (isDef(data)) registerDeepBindings(data)
    return vnode
  } else {
    return createEmptyVNode()
  }
}

可以看到 createElement 是对 _createElement 的封装,我们主要分析 _createElement 的实现,在 _createElement 方法中,首先是对数据的规范校验

数据规范校验

    1. data 中不能使用响应式对象作为属性
// data 选项中的属性不能使用响应式对象
if (isDef(data) && isDef((data: any).__ob__)) {
  process.env.NODE_ENV !== 'production' && warn(
    `Avoid using observed data object as vnode data: ${JSON.stringify(data)}\n` +
    'Always create fresh vnode data objects in each render!',
    context
  )
  return createEmptyVNode()
}
    1. 当特殊属性 key 的值为非字符串、非数字类型等非原始数据类型时
// warn against non-primitive key
if (process.env.NODE_ENV !== 'production' &&
  isDef(data) && isDef(data.key) && !isPrimitive(data.key)
) {
  if (!__WEEX__ || !('@binding' in data.key)) {
    warn(
      'Avoid using non-primitive value as key, ' +
      'use string/number value instead.',
      context
    )
  }
}

子节点规范化

接下来是对子节点规范化,虚拟 DOM 是由每个 VNode 以树状形式拼成的虚拟 DOM 树,因此我们需要保证每一个字节点都是 VNode 类型,这里需要对 _render 函数的两种来源分别进行分析

  • 用户定义的 render 函数。 在对用户定义的 render 函数进行规范化时,如果 childrenNode 为数组(例如 children 中有 v-for),则需要进行遍历;如果依旧存在数组,则进行递归
export function normalizeChildren (children: any): ?Array<VNode> {
  return isPrimitive(children)
    ? [createTextVNode(children)]
    : Array.isArray(children)
      ? normalizeArrayChildren(children)
      : undefined
}

function normalizeArrayChildren (children: any, nestedIndex?: string): Array<VNode> {
  const res = []
  let i, c, lastIndex, last
  for (i = 0; i < children.length; i++) {
    c = children[i]
    if (isUndef(c) || typeof c === 'boolean') continue
    lastIndex = res.length - 1
    last = res[lastIndex]
    //  nested
    if (Array.isArray(c)) {
      if (c.length > 0) {
        c = normalizeArrayChildren(c, `${nestedIndex || ''}_${i}`)
        // merge adjacent text nodes
        if (isTextNode(c[0]) && isTextNode(last)) {
          res[lastIndex] = createTextVNode(last.text + (c[0]: any).text)
          c.shift()
        }
        res.push.apply(res, c)
      }
    } else if (isPrimitive(c)) {
      if (isTextNode(last)) {
        // merge adjacent text nodes
        // this is necessary for SSR hydration because text nodes are
        // essentially merged when rendered to HTML strings
        res[lastIndex] = createTextVNode(last.text + c)
      } else if (c !== '') {
        // convert primitive to vnode
        res.push(createTextVNode(c))
      }
    } else {
      if (isTextNode(c) && isTextNode(last)) {
        // merge adjacent text nodes
        res[lastIndex] = createTextVNode(last.text + c.text)
      } else {
        // default key for nested array children (likely generated by v-for)
        if (isTrue(children._isVList) &&
          isDef(c.tag) &&
          isUndef(c.key) &&
          isDef(nestedIndex)) {
          c.key = `__vlist${nestedIndex}_${i}__`
        }
        res.push(c)
      }
    }
  }
  return res
}
  • 由模版编译得到的 render 函数 由模板编译得到的 render 函数都是 VNode 类型(函数式组件得到的是一个数组,这个后面再分析),这是我们只需要将整个 children 转换成一维数组
/**
 * 将数组扁平化,转换成一维数组
 * @param {*} children 
 * @returns 
 */
export function simpleNormalizeChildren (children: any) {
  for (let i = 0; i < children.length; i++) {
    if (Array.isArray(children[i])) {
      return Array.prototype.concat.apply([], children)
    }
  }
  return children
}