vue源码阅读三:虚拟 DOM 是如何生成的?(下)

1,716 阅读5分钟

上一篇:vue源码阅读二:虚拟 DOM 是如何生成的?(上)

前言

vue 源码系列的分享我会尽可能的表述清楚一些,简单一些。

createElement

上一节中我们知道 vue 生成虚拟 DOM 时候,使用的是 createElement 方法。下面我们就看下createElement方法的庐山真面目。

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方法是对_createElement方法的封装。真正创建虚拟 DOM 的是 _createElement 函数。继续看下_createElement方法。

_createElement

export function _createElement(
  context: Component, 
  tag?: string | Class<Component> | Function | Object, 
  data?: VNodeData, 
  children?: any, 
  normalizationType?: number 
): VNode | Array<VNode> {
  ...
  // 第一部分
  if (normalizationType === ALWAYS_NORMALIZE) {
    // render 函数是用户手写的时候调用,作用是将数组打平为一维数组
    children = normalizeChildren(children)
  } else if (normalizationType === SIMPLE_NORMALIZE) {
    // 当 render 是编译生成的时候调用,作用是将数组打平一层
    children = simpleNormalizeChildren(children)
  }
  ...
}

它先对 children 做个处理,是将 children 数组打平一个层级。

  • children 的处理

先看下简单点的 simpleNormalizeChildren 方法。

  • simpleNormalizeChildren
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
}

实际上是使用 Array.prototype.concat.apply([], children)children 数组直接打平一层。尝试手动调用下 _c 方法。

const h = vm._c;
console.log(h('div', null, ['test1', [h('p'), ['test2']]], 1));

结果如下,可以看到,只打平了一层数组。

  • normalizeChildren

normalizeChildren 方法则是,将children数组打平为一个一维数组。我们来看下它是如何实现的。

export function normalizeChildren (children: any): ?Array<VNode> {
  return isPrimitive(children)
    // 如果是原始类型(string、number、symbol、boolean),直接创建文本节点
    ? [createTextVNode(children)]
    : Array.isArray(children)
      // 如果是数组,调用 normalizeArrayChildren 方法
      ? normalizeArrayChildren(children) 
      : undefined
}

接下来,我们看下 normalizeArrayChildren 方法做了什么操作。

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 ,将数组内的元素都转为文本节点
        c = normalizeArrayChildren(c, `${nestedIndex || ''}_${i}`)
        if (isTextNode(c[0]) && isTextNode(last)) {
          // 合并文本节点,是将 res 的最后一个节点和 c 的第一个节点的文本内容进行合并
          res[lastIndex] = createTextVNode(last.text + (c[0]: any).text)
          c.shift()
        }
        // 利用 apply 将数组 c 的元素逐一 push 到 res 中,从而将多维数组转为一维数组
        // 相当于 res.push(...c)
        res.push.apply(res, c)
      }
    } else if (isPrimitive(c)) {
      if (isTextNode(last)) {
        // 如果 res 的最后一个元素是文本节点,将其文本与 c 进行合并
        res[lastIndex] = createTextVNode(last.text + c)
      } else if (c !== '') {
        // convert primitive to vnode
        res.push(createTextVNode(c))
      }
    } else {
      if (isTextNode(c) && isTextNode(last)) {
        // 如果 c 是文本节点,将 c 的文本与 res 的最后一个元素的文本进行合并
        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}__`
        }
        // 否则 c 已经是 VNode 类型了
        res.push(c)
      }
    }
  }
  return res
}

可以看到,normalizeArrayChildren的主要的作用是将 children 数组打平为一个一维数组,并且将 children 内的元素全部转为虚拟节点 VNode,res 最终返回的结果是 [VNode, VNode, ...]
我们尝试调用下$createElement方法。

const h = app.$createElement;
console.log(h('div', null, ['test1', [h('p'), ['test2']]], 1));

结果如下图,可以看到,children 数组被打平为一个一维数组了,并且children 数组内的元素全部被转为虚拟DOM了。

  • 生成虚拟 DOM

继续看_createElement第二部分代码。这部分比较简单,代码如下:

  // 第二部分
  if (typeof tag === 'string') {
    if (config.isReservedTag(tag)) {
      // tag 是内置的标签,如 div,创建普通元素 vnode
      vnode = new VNode(
        config.parsePlatformTagName(tag), data, children,
        undefined, undefined, context
      )
    } else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
      // 如果是已注册的组件名,创建一个组件类型的 vnode
      vnode = createComponent(Ctor, data, context, children, tag)
    } else {
      // 创建一个未知标签的 vnode
      vnode = new VNode(
        tag, data, children,
        undefined, undefined, context
      )
    }
  } else {
    // 如果 tag 不是字符串,则创建组件类型的 vnode
    vnode = createComponent(tag, data, context, children)
  }
  • 元素节点:直接使用 new VNode 直接生成虚拟DOM
  • 组件节点:使用 createComponent 生成虚拟DOM
    然鹅 createComponent 又是什么呢。
export function createComponent(
  Ctor: Class<Component> | Function | Object | void,
  data: ?VNodeData,
  context: Component,
  children: ?Array<VNode>,
  tag?: string
): VNode | Array<VNode> | void {
  // context.$options._base 对应的是 Vue 本身
  const baseCtor = context.$options._base
  if (isObject(Ctor)) {
    // 将组件对象 Ctor 转为 Vue 的子类,使其拥有 Vue 的完整的功能
    Ctor = baseCtor.extend(Ctor)
  }
  ...
  // 安装钩子函数,包含 init、prepatch、insert、destory,在将 vnode 转为 真实DOM时会用到
  installComponentHooks(data)
  const name = Ctor.options.name || tag // 用于拼接组件的tag
  const vnode = new VNode(
    `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`, // 对应tag
    data, // 父组件自定义事件和patch时用到的方法
    undefined, // children
    undefined, // text
    undefined, // 节点
    context, // 当前实例
    { Ctor, propsData, listeners, tag, children }, // 对应componentOptions属性
    asyncFactory
  )
  return vnode
}

这个有点绕,我们一点一点看吧。
首先,context.$options._base 是什么呢。这个是在 Vue 初始化时候,调用了 initGlobalAPI 函数。其中有两行代码是:

export function initGlobalAPI (Vue: GlobalAPI) {
  ... 
  // 经过 _init() 中的 mergeOptions 后,vm.$options 中的 _base 也就会等于 Vue
  Vue.options._base = Vue
  initExtend(Vue)
}

所以,baseCtor 就是 Vue 本身。而 initExtend 是什么呢。

export function initExtend(Vue: GlobalAPI) {
  Vue.extend = function (extendOptions: Object): Function {
    extendOptions = extendOptions || {}
    // vue 基类构造函数
    const Super = this
    ...
    // 定义一个VueComponent构造函数
    const Sub = function VueComponent(options) {
      this._init(options) // 继承 Vue 的 _init 方法
    }
    // 继承基类 Vue 的原型方法
    Sub.prototype = Object.create(Super.prototype)
    Sub.prototype.constructor = Sub
    // 合并options 
    Sub.options = mergeOptions(
      Super.options, // Vue 自身的options
      extendOptions // 用户手写的options
    )
    // 将基类Super的静态方法赋值给子类Sub
    Sub.extend = Super.extend
    Sub.mixin = Super.mixin
    Sub.use = Super.use
    ...
    return Sub
  }
}

所以,vue.extend() 会返回一个 VueComponent 函数,它拥有 vue 的完整功能。

installComponentHooks(data)

installComponentHooks安装一些钩子,如 init、prepatch、insert、destory,在将 vnode 转为 真实 DOM 时会用到。
createComponent最后一部分,就是创建组件类型的虚拟 DOM。

总结

  • createElement_createElement 做了个封装。_createElement 包含了两部分,一是 children 的处理,一是创建虚拟 DOM
  • children 的处理,根据 render 的不同,处理方式也有所区别。
    • render 是由 template 编译产生时,使用的是 simpleNormalizeChildrenchildren 数组降低一层。
    • render 是用户手写传入时,使用的是 normalizeChildrenchildren 数组转为一个一维数组,其内部的元素全部由 vnode 组成。
  • 创建虚拟DOM时。
    • 若是元素节点,直接使用 new Vnode() 创建虚拟DOM
    • 若是创建组件节点,则使用 createComponent ,它的作用,一是将组件对象转为 Vue 的子类,使其具有 Vue 的完整功能。二是初始化一些将虚拟DOM转为真实DOM的钩子。三是使用 new Vnode()创建组件类型的虚拟DOM,并将组件相关的信息保存在 componentOptions 中。