Vue 源码(一)如何创建VNode

3,362 阅读14分钟

前言

这篇文章直接从挂载开始说起,不会从new Vue开始,因为已经有很多文章讲解过;其实这篇文章也是这样,但是感觉还是记一下,也算为后面做铺垫了。

知识点

通过这篇文章可以了解如下内容

  • Vue 的挂载过程
  • Render Watcher 是什么
  • Render Watcher 的作用
  • 什么是虚拟 DOM
  • 虚拟DOM 的作用
  • Vue.extend 原理

挂载过程

整个Vue周期中,有3种方式会执行挂载过程

自动调用

optionsel 属性时,在 this._init() 中会自动执行

Vue.prototype._init = function (options?: Object) {
  	...
    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
}

手动调用

通过new Vue().$mount('#app')挂载

创建组件实例时

const componentVNodeHooks = {
  init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
    if (
    // ...
    } else {
      // ...
      
      child.$mount(hydrating ? vnode.elm : undefined, hydrating)
    }
  },
  prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {},
  insert (vnode: MountedComponentVNode) {},
  destroy (vnode: MountedComponentVNode) {}
}

这块会在后面patch过程中说,现在就知道这里会调用子组件实例的$mount方法去挂载子组件就行

上面这三种最终都是调用$mount方法去挂载实例。

Vue.prototype.$mount

对于 runtime-with-compiler 版本

先看一下 src/platform/web/entry-runtime-with-compiler.js 文件中定义:

// 缓存 src/platform/web/runtime/index.js 中定义的 $mount 方法
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  // 获取 DOM 节点
  el = el && query(el)

  // 如果 el 是 body 或者 html 则报错
  if (el === document.body || el === document.documentElement) {
    process.env.NODE_ENV !== 'production' && warn(
      `Do not mount Vue to <html> or <body> - mount to normal elements instead.`
    )
    return this
  }
  const options = this.$options
  // 优先级 render > template > el
  if (!options.render) {
    // 如果没有 render 属性,则获取 options.template
    let template = options.template
    if (template) {
      if (typeof template === 'string') {
        if (template.charAt(0) === '#') {
          // 如果 template 的值是以 # 开头,说明 template 的属性值是一个 id
          // 根据 template 获取 template.innerHTML
          template = idToTemplate(template)
          // 即没有定义 options.render,也没有 options.template 对应的 innerHTML 则报错
          if (process.env.NODE_ENV !== 'production' && !template) {
            warn(
              `Template element not found or is empty: ${options.template}`,
              this
            )
          }
        }
      } else if (template.nodeType) {
        // 如果 template 是一个 DOM 对象,则直接获取 innerHTML
        template = template.innerHTML
      } else {
        // 如果 template 即不是字符串,又不是 DOM,则报错
        if (process.env.NODE_ENV !== 'production') {
          warn('invalid template option:' + template, this)
        }
        return this
      }
    } else if (el) {
      // 如果 没有定义 render 也没有定义 template,则根据 el 去获取 el.outerHTML
      template = getOuterHTML(el)
    }
    if (template) {
      // 将  template 转为 render 函数(编译过程,后面会说)
      const { render, staticRenderFns } = compileToFunctions(template, {}, this)
      options.render = render
      options.staticRenderFns = staticRenderFns
    }
  }
  // 执行 src/platform/web/runtime/index.js 中定义的 $mount 方法
  return mount.call(this, el, hydrating)
}

这段代码首先缓存了原型上的 $mount 方法,再重新定义该方法

重新定义的方法对 el 做了限制,Vue 不能挂载在 bodyhtml 这样的根节点上。 如果没有定义 render 方法,则会把 el 或者 template 字符串转换成 render 方法,并把 render 函数绑定到 options 上,然后执行src/platform/web/runtime/index.js 中定义的 $mount 方法

原先原型上的 $mount 方法在 src/platform/web/runtime/index.js 中定义,之所以这么设计完全是为了复用,因为它是可以被 runtime only 版本的 Vue 直接使用

Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  // 获取 el 的 DOM 对象
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}

$mount 方法支持传入 2 个参数,第一个是 el,它表示挂载的元素,可以是字符串,也可以是 DOM 对象,如果是字符串在浏览器环境下会调用 query 方法转换成 DOM 对象。第二个参数是和服务端渲染相关,在浏览器环境下不需要传第二个参数。

接下来调用 mountComponent方法,并把当前 Vue 实例、elhydrating 传入

mountComponent方法定义在 src/core/instance/lifecycle.js 文件中:

export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  // 在这里会设置一次 $el
  vm.$el = el
  if (!vm.$options.render) {
    // ... 如果没有 render 函数就报错
  }
  // 执行 beforeMount,先父后子
  callHook(vm, 'beforeMount')
  let updateComponent
  /* istanbul ignore if */
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    /* ... */
  } else {
    updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }
  }
  // 创建 Render Watcher
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher 这里将是 true  */)
  
  hydrating = false
  // 只有根组件没有 $vnode
  if (vm.$vnode == null) {
    // 表示此组件已经挂载完成
    vm._isMounted = true
    // 根组件的 mounted 函数 先子后父
    callHook(vm, 'mounted')
  }
  return vm
}

从上面的代码可以看到,mountComponent 核心就是先实例化一个Render Watcher,实例化过程中会调用 updateComponent方法,在此方法中调用 vm._render 方法生成VNode,最终调用 vm._update 更新 DOM。

Watcher 在这里起到两个作用,一个是初始化的时候会执行回调函数,另一个是当 vm 实例中被监测数据发生改变时也会执行回调函数。

函数最后,会将根实例的 vm._isMounted 设置为 true, 表示这个实例已经挂载了,同时执行根实例的mounted 钩子函数。 这里注意 vm.$vnode 是组件占位符VNode,所以它为 Null 则表示当前是根 Vue 的实例。

挂载过程总结

对于 runtime-with-compiler 版本的挂载过程是,根据templateel获取render函数,创建Render Watcher,在创建Render Watcher的过程中触发挂载过程;即调用render函数获取渲染VNode,调用patch函数创建节点并渲染到页面上。

对于runtime版本,由于在打包构建的时候已经将模版转成了render函数,所以省略了获取render的步骤;会直接创建Render Watcher,在创建Render Watcher的过程中触发挂载过程;即调用render函数获取渲染VNode,调用patch函数创建节点并渲染到页面上。

在创建Render Watcher之前会调用组件的 beforeMount 钩子函数

Render Watcher 是什么

每一个组件都对应一个 Watcher 对象,包含如下内容

  • 存储组件的 Vue 实例
  • 触发视图更新的方法
  • 存储当前组件使用到的响应式属性的 Dep 实例

Render Watcher 的作用

Vue使用发布订阅模式实现数据的双向绑定。其中 Render Watcher 就是里面的订阅者。当数据更新会通知Render Watcher更新视图。在Vue 源码(二)响应式原理中会详细说明

render 函数是怎么创建 VNode 的

render 函数的目的是创建并返回 VNode,总共有两个过程会执行render函数:

  1. 我们知道每个组件都会创建一个Render Watcher。也就是说每个组件在创建Render Watcher的时候都会执行updateComponent方法
  2. 当组件更新时,会再次执行 updateComponent
updateComponent = () => {
  vm._update(vm._render(), hydrating)
}

updateComponent函数内部,调用_render函数创建VNode。

_render

_render() 方法是实例的一个私有方法,它用来把实例渲染成一个VNode。它的定义在 src/core/instance/render.js 文件中:

Vue.prototype._render = function (): VNode {
  const vm: Component = this
  // 获取render函数和组件占位符VNode(如果有的话)
  const { render, _parentVnode } = vm.$options
  if (_parentVnode) {
    // 规范化插槽作用域和插槽
    vm.$scopedSlots = normalizeScopedSlots(
      _parentVnode.data.scopedSlots,
      vm.$slots,
      vm.$scopedSlots
    )
  }
  // 将 组件的占位符 vnode 赋值给 vm.$vnode
  vm.$vnode = _parentVnode
  let vnode
  try {
    currentRenderingInstance = vm
    // 执行组件的 render 函数 
    vnode = render.call(vm._renderProxy, vm.$createElement)
  } catch (e) {
    // ...
    } finally {
    currentRenderingInstance = null
  }
  if (Array.isArray(vnode) && vnode.length === 1) {
    vnode = vnode[0]
  }

  // 将 组件的占位符 vnode 赋值给 vnode.parent
  vnode.parent = _parentVnode
  return vnode
}

_render方法的核心就是执行组件的render函数,并返回 VNode。这个 VNode 包含当前组件中普通标签VNode,以及组件标签的占位符VNode;这两种VNode 的数据结构会在后面说一下

定义/编译后的render函数接收createElement | h方法,而这个方法就是vm.$createElement方法

vnode = render.call(vm._renderProxy, vm.$createElement)

vm.$createElement

export function initRender (vm: Component) {
  // 给被模板编译成的 render 函数使用的
  vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
  // 给开发者手写的 render 函数使用的
  vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
}

这两个函数接收的参数相同,就是render函数中传给createElement | h方法的参数。参数文档

唯一的区别就是最后一个参数,对于开发者手写的render函数始终为true,而通过编译生成的render函数,已经设置好参数d(子节点)了。createElement最后一个参数表示对子节点的规范化方式,类型不同规范的方法也就不一样,后面会说

注意点

执行vm._render时,有两个需要注意的点

  • vm.$vnode = _parentVnode组件实例的$vnode属性指向组件的占位符 VNode
  • vnode.parent = _parentVnode组件渲染VNode的parent属性指向组件的占位符 VNode

继续向下,执行定义/编译的render函数时,会执行createElement方法

createElement

createElement函数定义在 src/core/vdom/create-element.js

const SIMPLE_NORMALIZE = 1
const ALWAYS_NORMALIZE = 2

export function createElement (
  context: Component,
  tag: any,
  data: any,
  children: any,
  normalizationType: any,
  alwaysNormalize: boolean
): VNode | Array<VNode> {
  //如果没有传 data 参数,改变实参和形参顺序
  if (Array.isArray(data) || isPrimitive(data)) {
    normalizationType = children
    children = data
    data = undefined
  }
  // 如果传入的 alwaysNormalize 为 true,则将 normalizationType 赋值成数字 2
  if (isTrue(alwaysNormalize)) {
    normalizationType = ALWAYS_NORMALIZE
  }
  return _createElement(context, tag, data, children, normalizationType)
}

createElement 方法实际上是对 _createElement 方法的封装,它允许传入的参数更加灵活,在处理这些参数后,调用真正创建 VNode 的函数 _createElement

export function _createElement (
  context: Component,
  tag?: string | Class<Component> | Function | Object,
  data?: VNodeData, // data 是标签上的属性或者 render 函数的第二个参数
  children?: any,
  normalizationType?: number
): VNode | Array<VNode> {
  // 不能将响应式对象当作 VNode 参数传入,报错并返回空 VNode 节点
  if (isDef(data) && isDef((data).__ob__)) {
    process.env.NODE_ENV !== 'production' && warn()
    return createEmptyVNode()
  }
  // 动态组件
  if (isDef(data) && isDef(data.is)) {
    tag = data.is
  }
  if (!tag) {
    return createEmptyVNode()
  }

  if (Array.isArray(children) &&
    typeof children[0] === 'function'
  ) {
    data = data || {}
    data.scopedSlots = { default: children[0] }
    children.length = 0
  }
  // 在这里规范化 children 
  // ALWAYS_NORMALIZE = 2
  // SIMPLE_NORMALIZE = 1
  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 ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
      // 如果 tag 是字符串,并且不是平台保留标签,则说明是一个组件标签
      // resolveAsset 根据传入的 tag 去 options.components 属性中查找对应组件的导出内容
      // 此时 Ctor 可能是一个对象(同步组件),也可能是一个函数(异步组件)
      // component
      vnode = createComponent(Ctor, data, context, children, tag)
    } else {
      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函数会规范化children,主要根据normalizationType的不同,调用不同的规范化函数;

接下来就是根据tag做不同的处理:

  • 如果tag是字符串:
    • tag是平台保留标签,创建渲染VNode
    • tag不是平台保留标签,并且vm.$options.components中有名为tag的属性,创建组件占位符VNode
    • 如果上述都不是,创建渲染VNode
  • 如果tag不是字符串:说明传入的tag是一个组件的对象,比如render: h => App,创建组件占位符 VNode
  • 最后返回VNode

render.jpg

createComponent

组件占位符 VNode 通过 createComponent 方法创建

createComponent 定义在 src/core/vdom/create-component.js

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
  }
  // src/core/global-api/index.js 中的 initGlobalAPI
  // 指向 Vue 构造函数
  const baseCtor = context.$options._base
  // 如果 Ctor 是一个对象,则通过 Vue.extend 创建子组件构造函数,并赋值给 Ctor
  if (isObject(Ctor)) {
    Ctor = baseCtor.extend(Ctor)
  }
	
  if (typeof Ctor !== 'function') {
    // ...如果 Ctor 不是函数则报错
    return
  }

  // 异步组件相关,后面会说
  let asyncFactory
  if (isUndef(Ctor.cid)) {/* ... */}
  data = data || {}
  // resolve constructor options in case global mixins are applied after
  // component constructor creation
  resolveConstructorOptions(Ctor)

  // 处理组件 v-model
  if (isDef(data.model)) {
    transformModel(Ctor.options, data)
  }

  // 提取传入的 props 值
  const propsData = extractPropsFromVNodeData(data, Ctor, tag)

  // functional component
  if (isTrue(Ctor.options.functional)) {
    return createFunctionalComponent(Ctor, propsData, data, context, children)
  }
  // 拿到自定义事件
  const listeners = data.on
  // 将 nativeOn 赋值给 data.on
  data.on = data.nativeOn
  if (isTrue(Ctor.options.abstract)) {
    const slot = data.slot
    data = {}
    if (slot) {
      data.slot = slot
    }
  }

  // 给组件占位符 VNode 添加 hook 钩子
  installComponentHooks(data)

  // 获取 组件名称
  const name = Ctor.options.name || tag
  // 创建组件占位符 VNode
  // 注意这里将  { Ctor, propsData, listeners, tag, children } 赋值给了 VNode 的 componentOptions 属性
  // 并且 children 为 undefined
  const vnode = new VNode(
    `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
    data, undefined, undefined, undefined, context,
    { Ctor, propsData, listeners, tag, children },
    asyncFactory
  )
  return vnode
}

createComponent函数接收 5 个参数:

  • Ctor:组件信息
  • data:标签上的属性或者render函数的第二个参数
  • context:父组件 Vue 实例
  • children:组件子节点
  • tag:组件名

createComponent函数作用:

  • 为当前组件创建构造函数 go
  • 处理组件v-model
  • 提取传入的propsgo
  • 获取自定义事件,并将nativeOn赋值给data.on
  • data添加 hook 钩子函数 go
  • 创建组件占位符VNode,并将{ Ctor, propsData, listeners, tag, children } 赋值给了 VNode 的 componentOptions属性
  • 返回组件占位符VNode

render2.jpg

_render函数的总流程大致就是上图的样子

小结

先看下组件占位符 VNode 和 渲染VNode 的数据结构

组件占位符 VNode 数据结构

假设组件标签如下

<HelloWorld id="js_hello" :flag="flag" msg="Welcome to Your Vue.js App"/>
{
    // 组件占位符 VNode 特有的属性
    componentInstance: undefined, // 子组件的 Vue 实例,子组件Vue实例创建完成后赋值
    componentOptions: { // 组件占位符 VNode 特有的属性
      propsData: { // 根据子组件定义的 props 属性,获取传入子组件的值
          flag: 1,
          msg"Welcome to Your Vue.js App"
      }
      listeners: undefined, // 自定义事件
      tag: 'HelloWorld', // 组件名
      children: undefined, // 插槽内容
      Ctor: ƒ // 构建子组件 Vue实例 的构造函数
    },
    context: {…}, // 父组件的 Vue实例,
    data: { // 组件标签上的属性或者 render 函数的第二个参数
      attrs: { // 组件标签上的属性,不包含子组件中定义的 props 属性
          id: 'js_hello'
      },
      on: undefined, // 有 .native 修饰符的事件
      hook: {…}  // 钩子函数,在 patch 的不同时机触发
    },
    elm: DOM, // 组件根元素,在 patch 过程创建
    tag: "vue-component-2-HelloWorld"
}

组件占位符VNode 里面存储子组件Vue实例的构造函数、以及存储传给子组件的数据。他的作用其实就是一个占位符。当子组件的DOM创建完成,会将 DOM 赋值给 组件占位符VNode 的elm 属性。而渲染时使用的就是这个elm

渲染VNode 数据结构

假设组件内容如下

<template>
  <div class="hello">
    <h1>{{ msg }}</h1>
  </div>
</template>
{
    children: [{  // 子元素
        context: {}, 
        data: { // 组件标签上的属性或者 render 函数的第二个参数
            staticClass: 'hello',
        },
        elm: undefined, // 根的 DOM 元素
        parent: undefined, // 组件的组件占位符VNode,只有组件 根VNode 才有
        tag: 'h1',
        children:[{
          text: "Welcome to Your Vue.js App",
          context: undefined,
          data: undefined,
          elm: undefined,
          parent: undefined,
          tag: undefined
        }]
    }],
    context: {}, // 当前组件的Vue实例
    data: { // 组件标签上的属性或者 render 函数的第二个参数
        staticClass: 'hello',
    },
    elm: undefined, // 根的 DOM 元素
    parent: {}, // 组件的组件占位符VNode,只有组件 根VNode 才有
    tag: 'div'
}

组件占位符VNode 和渲染VNode 的区别

  • 组件占位符VNode,是一个占位符;描述的是组件标签。存储传递给子组件的信息componentOptions
  • 渲染VNode,描述普通标签。存储标签信息。

DEMO

假设有这样的组件关系

new Vue({
  render: h => h(App)
}).$mount('#app')

# App.vue
<div>
    <HelloWorld id="js_hello" :flag="flag" msg="Welcome to Your Vue.js App"/>
</div>
# helloWorld.vue
<div class="hello">
    <h1>{{ msg }}</h1>
</div>

VNode 创建流程如下:

首先创建根实例的Render Watcher,执行render函数,创建App组件的组件占位符VNode。根据VNode构建 DOM 树。构建 DOM 树过程中发现有组件占位符VNode(App组件的),会根据组件占位符VNode的信息创建App组件的Vue实例;并执行App组件的挂载过程,也就是mount方法;然后创建App组件的Render Watcher,执行render函数,创建App组件的渲染VNode,其中就包含helloWorld的组件占位符VNode。之后都是这个流程。在构建App组件的 DOM 树时,根据helloWorld的组件占位符VNode 创建helloWorld组件的Vue实例,并执行挂载过程;创建helloWorld组件的渲染VNode;然后构建helloWorld组件的 DOM 树。并将构建的 DOM 树挂载到helloWorld组件占位符VNode 的elm属性上。

说一下支线

规范化 Children

Virtual DOM 实际上是一个树状结构,每一个 VNode 可能会有若干个子节点,这些子节点应该也是 VNode 的类型。_createElement 接收的第 4 个参数 children 是任意类型的(可能是字符串、数组、嵌套数组等),因此需要把它们 规范成深度只有一层的 VNode 数组

_createElement 方法中根据 normalizationType 的不同,调用了 normalizeChildren(children)simpleNormalizeChildren(children) 方法

if (normalizationType === ALWAYS_NORMALIZE) {
  children = normalizeChildren(children)
} else if (normalizationType === SIMPLE_NORMALIZE) {
  children = simpleNormalizeChildren(children)
}

simpleNormalizeChildren

它们的定义都在 src/core/vdom/helpers/normalzie-children.js 中:

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
}

simpleNormalizeChildren 把整个 children 数组打平,让它的深度只有一层;这种针对于某些通过编译生成的 render 函数,比如没有v-for作用域插槽render函数

normalizeChildren

normalizeChildren 相对就比较复杂了

export function normalizeChildren (children: any): ?Array<VNode> {
  return isPrimitive(children) // 判断 children 是不是基本数据类型
    ? [createTextVNode(children)] // 如果是将 children 修改成文本节点并返回
    : Array.isArray(children)
      ? normalizeArrayChildren(children)
      : undefined
}

normalizeChildren最终返回的是 规范化好的 VNode 数组,而且深度只有一层

逻辑如下:

  • children 是基本数据类型,根据children创建文本节点并返回;
  • children 是数组,调用normalizeArrayChildren方法
  • 即不是数组又不是基本数据类型,返回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
    // 获取 res 数组中最后一个 VNode
    last = res[lastIndex]
    //  如果 c 是一个数组
    if (Array.isArray(c)) {
      if (c.length > 0) {
        c = normalizeArrayChildren(c, `${nestedIndex || ''}_${i}`)
        // 如果 c 的第一个元素和 last (res 的 最后一个元素)都是文本节点,就合并两个节点
        if (isTextNode(c[0]) && isTextNode(last)) {
          res[lastIndex] = createTextVNode(last.text + (c[0]: any).text)
          // 删除 c 的第一个节点
          c.shift()
        }
        // 将 c 中所有元素 push 到 res 中
        res.push.apply(res, c)
      }
    } else if (isPrimitive(c)) { // 如果 c 是基本数据类型
      if (isTextNode(last)) {
        // 如果 res 最后一个元素是 文本节点,则将最后一个元素和 c 合并
        res[lastIndex] = createTextVNode(last.text + c)
      } else if (c !== '') {
        // last 不是文本节点
        res.push(createTextVNode(c))
      }
    } else { // c 既不是数组也不是基本数据类型
      if (isTextNode(c) && isTextNode(last)) {
        // 合并
        res[lastIndex] = createTextVNode(last.text + c.text)
      } else {
        // 如果是循环列表,列表还存在嵌套的情况,并且c 没有 key 属性,则根据 nestedIndex 去设置它的 key
        if (isTrue(children._isVList) &&
          isDef(c.tag) &&
          isUndef(c.key) &&
          isDef(nestedIndex)) {
          c.key = `__vlist${nestedIndex}_${i}__`
        }
        res.push(c)
      }
    }
  }
  return res
}

normalizeArrayChildren 主要的逻辑就是遍历 children,获得单个节点 c,然后判断 c 的类型,如果是一个数组,则递归调用 normalizeArrayChildren; 如果是基础类型,则通过 createTextVNode 方法转换成 VNode 类型;否则就已经是 VNode 类型了,如果 children 是一个列表并且列表还存在嵌套的情况,则根据 nestedIndex 去更新它的 key。这里需要注意一点,在遍历的过程中,对这 3 种情况都做了如下处理:如果存在两个连续的 text 节点,会把它们合并成一个 text 节点。

经过对 children 的规范化,children 变成了一个VNode数组。

创建组件构造函数 - Vue.extend

在创建组件占位符 VNode 时,会先通过Vue.extend创建对应组件的构造函数,并赋值给 Ctor

if (isObject(Ctor)) {
    Ctor = baseCtor.extend(Ctor)
}

Vue.extend函数的定义,在src/core/global-api/extend.js

export function initExtend (Vue: GlobalAPI) {
  Vue.cid = 0
  let cid = 1
  Vue.extend = function (extendOptions: Object): Function {
    // extendOptions: 组件导出的内容
    extendOptions = extendOptions || {}
    const Super = this
    const SuperId = Super.cid
    // 获取缓存的组件列表
    const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
    // 如果 cachedCtors 中有 key 是 SuperId 的属性值,直接返回
    // 避免多次继承同一个构造函数
    if (cachedCtors[SuperId]) {
      return cachedCtors[SuperId]
    }

    const name = extendOptions.name || Super.options.name
    
    // 定义 组件构造函数
    const Sub = function VueComponent (options) {
      this._init(options)
    }
    // 原型继承
    Sub.prototype = Object.create(Super.prototype)
    Sub.prototype.constructor = Sub
    // 设置组件构造函数的 cid
    Sub.cid = cid++
    // 合并 options 配置
    Sub.options = mergeOptions(
      Super.options,
      extendOptions
    )
    Sub['super'] = Super
    // 初始化 props
    if (Sub.options.props) {
      initProps(Sub)
    }
    // 初始化 computed
    if (Sub.options.computed) {
      initComputed(Sub)
    }

    Sub.extend = Super.extend
    Sub.mixin = Super.mixin
    Sub.use = Super.use
    // 注册 Vue.component,Vue.directive, Vue.filter 函数
    ASSET_TYPES.forEach(function (type) {
      Sub[type] = Super[type]
    })
    // 递归组件,是通过 name 去寻找
    if (name) {
      Sub.options.components[name] = Sub
    }
    Sub.superOptions = Super.options
    Sub.extendOptions = extendOptions
    Sub.sealedOptions = extend({}, Sub.options)

    // 将 Sub 缓存到 extendOptions._Ctor 中,key 是 Super.cid
    cachedCtors[SuperId] = Sub
    return Sub
  }
}

Vue.extend 原理

Vue.extend 的作用就是构造一个Vue的子类,它使用原型继承的方式创建了一个继承于Vue的构造器 Sub,并返回。然后对Sub这个对象本身扩展了一些属性,如扩展options、添加全局 API 等;并且对配置中的propscomputed做了初始化工作;将其代理到构造函数的原型上,目的是,每次实例化组件实例时不需要再次代理,从而减少代码执行次数。最后对Sub构造函数做了缓存,避免多次继承同一个构造函数。

initProps

这是一种优化手段

// state.js
// 设置代理,将 key 代理到 target 上
export function proxy (target: Object, sourceKey: string, key: string) {
  sharedPropertyDefinition.get = function proxyGetter () {
    return this[sourceKey][key]
  }
  sharedPropertyDefinition.set = function proxySetter (val) {
    this[sourceKey][key] = val
  }
  Object.defineProperty(target, key, sharedPropertyDefinition)
}
// 当前 js
function initProps (Comp) {
  const props = Comp.options.props
  for (const key in props) {
    proxy(Comp.prototype, `_props`, key)
  }
}

这个函数比较简单,就是通过Object.defineProperty_props 代理到 Sub的原型上。在这里做的目的是,每次实例化当前组件时不需要再对 _props 做代理,因为原型中已经做过代理了

// 创建构造函数期间将 props 挂载到了原型上并添加了代理,所以通过 in 方法会访问到相应的 key
if (!(key in vm)) {
  proxy(vm, `_props`, key)
}

initComputed

作用和initProps 相同,将options中的计算属性代理到Sub的原型上,并设置 存描述符 getter

function initComputed (Comp) {
  const computed = Comp.options.computed
  for (const key in computed) {
    // 将组件的计算属性挂载到 组件构造函数的原型上
    defineComputed(Comp.prototype, key, computed[key])
  }
}

提取传入的 props 值

const propsData = extractPropsFromVNodeData(data, Ctor, tag)

extractPropsFromVNodeData 根据子组件的options.props中的属性,获取父组件传递给子组件的prop数据

export function extractPropsFromVNodeData (
  data: VNodeData,
  Ctor: Class<Component>,
  tag?: string
): ?Object {
  // 获取组件中 props 配置
  const propOptions = Ctor.options.props
  if (isUndef(propOptions)) {
    return
  }
  const res = {}
  // 获取 attrs 和 props
  const { attrs, props } = data
  if (isDef(attrs) || isDef(props)) {
    for (const key in propOptions) {
      // 如果 key 是驼峰式,将其转为 连线符的形式
      const altKey = hyphenate(key)
      if (process.env.NODE_ENV !== 'production') {
        // 将 key 转为小写 aBc -> abc、a-b -> a-b
        const keyInLowerCase = key.toLowerCase()
        // key !== keyInLowerCase 说明 key 是驼峰式
        if (
          key !== keyInLowerCase &&
          attrs && hasOwn(attrs, keyInLowerCase)
        ) {
          // key 是驼峰式,但传入的属性即不是驼峰式,也不是连线符的形式,则报错
          tip(/* ... */)
        }
      }
      // 从 props 和 attrs 中根据 key 查找传入子组件的属性,并赋值给 res
      // props 优先级高于 attrs,如果 props 中存在对应的 key,则不会再去 attrs 中查找
      checkProp(res, props, key, altKey, true) ||
      checkProp(res, attrs, key, altKey, false)
    }
  }
  return res
}

function checkProp (
  res: Object, // 结果对象
  hash: ?Object, // props、attrs
  key: string,
  altKey: string,  // 连线符形式的 key
  preserve: boolean // 为 true, 删除 hash 中属性名为 key 的属性
): boolean {
  if (isDef(hash)) {
    if (hasOwn(hash, key)) {
      res[key] = hash[key]
      if (!preserve) {
        delete hash[key]
      }
      return true
    } else if (hasOwn(hash, altKey)) { // 查找连线符形式的 key 
      res[key] = hash[altKey]
      if (!preserve) {
        delete hash[altKey]
      }
      return true
    }
  }
  // 没找到的话返回 false
  return false
}

extractPropsFromVNodeData根据组件定义的 props属性,从 data 中的 propsattrs查找属性值为key 的属性,添加到 res中并返回。

优先从 data.props 中查找

  • 如果没找到 checkProp 返回 false,会执行checkProp(res, attrs, key, altKey, false),再从 data.attrs 中查找;
  • 如果data.props 中找到了,返回true,查找结束
  • data.props找到的话,不会删除 data.props 中的属性,而data.attrs会删除

createComponent内,extractPropsFromVNodeData的返回值最终会放到 组件占位符 VNode 的componentOptions

添加组件钩子函数

installComponentHooks(data)

在初始化组件占位符 VNode 的过程中会将以下这几个钩子函数添加到 data.hook 中,并在 patch过程的对应时机,执行对应钩子函数

const componentVNodeHooks = {
  init (vnode: VNodeWithData, hydrating: boolean): ?boolean {},
  prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {},
  insert (vnode: MountedComponentVNode) {},
  destroy (vnode: MountedComponentVNode) {}
}
const hooksToMerge = Object.keys(componentVNodeHooks)

installComponentHooks

整个 installComponentHooks 的过程就是把 componentVNodeHooks 的钩子函数根据合并策略合并到 data.hook

function installComponentHooks (data: VNodeData) {
  const hooks = data.hook || (data.hook = {})
  // 遍历 componentVNodeHooks 中的属性名
  for (let i = 0; i < hooksToMerge.length; i++) {
    const key = hooksToMerge[i]
    const existing = hooks[key]
    const toMerge = componentVNodeHooks[key]
    // 已有的 hook 不等于 componentVNodeHooks[key],并且已有的 hook 没有 _merged 属性
    if (existing !== toMerge && !(existing && existing._merged)) {
      hooks[key] = existing ? mergeHook(toMerge, existing) : toMerge
    }
  }
}

function mergeHook (f1: any, f2: any): Function {
  const merged = (a, b) => {
    f1(a, b)
    f2(a, b)
  }
  // 添加一个标识,防止重复添加 componentVNodeHooks 中的 hook 钩子
  merged._merged = true
  return merged
}

总结

什么是虚拟 DOM

用 js 对象模拟真实 DOM 树

虚拟 DOM 的作用

在react,vue等技术出现之前,要改变页面展示的内容只能通过遍历查询 DOM 树的方式找到需要修改的 DOM 然后修改样式行为或者结构,来达到更新 UI 的目的。

这种方式相当消耗计算资源,因为每次查询 DOM 几乎都需要遍历整颗 DOM 树,如果建立一个与 DOM 树对应的虚拟 DOM 对象( js 对象),以对象嵌套的方式来表示 DOM 树及其层级结构,那么每次 DOM 的更改就变成了对 js 对象的属性的增删改查,这样一来查找 js 对象的属性变化要比查询 DOM 树的性能开销小。

Vue 的挂载过程

  • 对于 runtime-with-compiler 版本的挂载过程是,根据templateel获取render函数,创建Render Watcher,在创建Render Watcher的过程中触发挂载过程;即调用render函数获取渲染VNode,调用patch函数创建节点并渲染到页面上。

  • 对于runtime版本,由于在打包构建的时候已经将模版转成了render函数,所以省略了获取render的步骤;会直接创建Render Watcher,在创建Render Watcher的过程中触发挂载过程;即调用render函数获取渲染VNode,调用patch函数创建节点并渲染到页面上。

Render Watcher 是什么

每一个组件都对应一个 Watcher 对象,包含如下内容

  • 存储组件的 Vue 实例
  • 触发视图更新的方法
  • 存储当前组件使用到的响应式属性的 Dep 实例

Render Watcher 作用

Vue使用发布订阅模式实现数据的双向绑定。其中 Render Watcher 就是里面的订阅者。当数据更新会通知Render Watcher更新视图。在Vue 源码(二)响应式原理中会详细说明

Vue.extend 原理

Vue.extend 的作用就是构造一个Vue的子类,它使用原型继承的方式创建了一个继承于Vue的构造器 Sub,并返回。然后对Sub这个对象本身扩展了一些属性,如扩展options、添加全局 API 等;并且对配置中的propscomputed做了初始化工作;将其代理到构造函数的原型上,目的是,每次实例化组件实例时不需要再次代理,从而减少代码执行次数。最后对Sub构造函数做了缓存,避免多次继承同一个构造函数。

最后

接下来会说datapropscomputedwatch的响应式原理。在创建VNode整个过程中,很多点都是和后面说的API有关联,在说到对应API时,会再过一遍。