Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
yangrenmu opened this issue Dec 27, 2019 · 0 comments
Open

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

yangrenmu opened this issue Dec 27, 2019 · 0 comments
Labels

Comments

@yangrenmu
Copy link
Owner

上一篇: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 中。
@yangrenmu yangrenmu added the vue label Dec 27, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant