✌vue源码之初始化 - new Vue() 之后发生了什么

1,278 阅读13分钟
<div id="app">
  {{ message }}
</div>

var app = new Vue({
  el: '#app',
  data: {
    message: 'Hello Vue!'
  }
})

这是 vue 的简单使用方式,Vue 是如何初始化页面的?创建一个 Vue 实例时又发生了什么?本文从源码上分析一下这部分主要流程(不是细节)

Vue 源码可读性很强的,看命名和注释基本上就能知道这部分代码是做什么的,要深入了解细节,进到对应的方法查看就行,这里只记录主要流程

Vue 构造函数

要了解 new Vue() 时发生了什么,首先要找到 Vue 构造函数 Vue 构造函数的定义在 src/core/instance/index.js

function Vue (options) {
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  this._init(options)
}

可以看到,创建 Vue 实例时,会调用 _init 方法进行初始化

初始化

_init 方法定义在 src/core/instance/init.js

export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
    // a uid
    vm._uid = uid++

    // 性能监测相关
    let startTag, endTag
    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
      startTag = `vue-perf-start:${vm._uid}`
      endTag = `vue-perf-end:${vm._uid}`
      mark(startTag)
    }

    // a flag to avoid this being observed
    vm._isVue = true
    // merge options
    if (options && options._isComponent) {
      // optimize internal component instantiation
      // since dynamic options merging is pretty slow, and none of the
      // internal component options needs special treatment.
      initInternalComponent(vm, options)
    } else {
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }
    /* istanbul ignore else */
    if (process.env.NODE_ENV !== 'production') {
      initProxy(vm)
    } else {
      vm._renderProxy = vm
    }
    // expose real self
    vm._self = vm
    initLifecycle(vm)
    initEvents(vm)
    initRender(vm)
    callHook(vm, 'beforeCreate')
    initInjections(vm) // resolve injections before data/props
    initState(vm)
    initProvide(vm) // resolve provide after data/props
    callHook(vm, 'created')

    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
      vm._name = formatComponentName(vm, false)
      mark(endTag)
      measure(`vue ${vm._name} init`, startTag, endTag)
    }

    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }
}

可以看到 _init 是 Vue 原型上的方法,这部分代码非常清晰,Vue 把各种初始化都抽成了单独的方法,需要维护哪部分代码就进到相应的方法修改即可,可读性、可维护性强,值得学习

这部分代码前面首先判断是否开启了 config.performance ,如果开启了会有一些性能监测相关的标识,然后是合并 options,就是将传入的 options 合并到 vm.$options,后面是生命周期执行以及一系列的初始化,看命名就知道是初始化哪部分了,最后调用 $mount 方法进行挂载

挂载

Vue 有两个版本runtime-onlyruntime-compiler,如果要使用 template 属性,则需要引入包含编译器的 runtime-compiler 版本,这个版本的源码包含将 template 转为 render 的代码,所以相对也会更大,占用更多资源,所以更推荐使用 runtime-only 版本。

// 要用 template 的话需要引入 runtime-compiler 版本
new Vue({
  template: '<div>TTT</div>'
})

看 mount 源码前先看看 Vue 几种使用方式,这几种情况在 mount 方法中都有判断处理

// 有 render
new Vue({
  el: '#app',
  render: function (h) {
    return h('h1', 'title')
  }
})

// 无 render 有 template
new Vue({
  el: '#app',
  template: '<h2>Title</h2>'
})

// 无 render 无 template 有 el
new Vue({
  el: '#app'
})

下面是 runtime-compiler 版本的 mount 源码,在 src/platform/web/entry-runtime-with-compiler.js 文件中

const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && query(el)

  /* istanbul ignore if */
  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
  // resolve template/el and convert to render function
  if (!options.render) {
    let template = options.template
    if (template) { // 无 render 有 template
      if (typeof template === 'string') {
        // template 是一个 id 则调用 idToTemplate
        if (template.charAt(0) === '#') {
          template = idToTemplate(template)
          /* istanbul ignore if */
          if (process.env.NODE_ENV !== 'production' && !template) {
            warn(
              `Template element not found or is empty: ${options.template}`,
              this
            )
          }
        }
      } else if (template.nodeType) { // template.nodeType 大于 0 说明 tempalte 传的是一个节点,这时直接获取 innerHTML
        template = template.innerHTML
      } else {
        if (process.env.NODE_ENV !== 'production') {
          warn('invalid template option:' + template, this)
        }
        return this
      }
    } else if (el) { // 无 render 无 template 有 el
      template = getOuterHTML(el)
    }
    if (template) { // 拿到 template 后,通过 compileToFunctions 得到 render
      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile')
      }

      const { render, staticRenderFns } = compileToFunctions(template, {
        shouldDecodeNewlines,
        shouldDecodeNewlinesForHref,
        delimiters: options.delimiters,
        comments: options.comments
      }, this)
      options.render = render
      options.staticRenderFns = staticRenderFns

      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile end')
        measure(`vue ${this._name} compile`, 'compile', 'compile end')
      }
    }
  }
  return mount.call(this, el, hydrating) // 有 render 则直接挂载
}

可以看到这段代码先缓存了 没有编译 template 功能的 $mount 方法,再进行重写,重写部分主要是在各种情况下获取到 template ,然后通过 compileToFunctions 将 template 转化为 render,得到 render 后,再调用缓存起来的 mount 方法,这样就达到了复用的目的,相当于在原本的 $mount 基础上加上了编译 template 的功能

缓存的 mount 方法定义在 src/platform/web/runtime/index.js 文件,runtime-only 版本不需编译 template ,直接就调用这个方法

// runtime-only 版本不需转换 template ,直接就调用这个方法
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}

// query 方法
export function query (el: string | Element): Element {
  if (typeof el === 'string') {
    const selected = document.querySelector(el)
    if (!selected) {
      process.env.NODE_ENV !== 'production' && warn(
        'Cannot find element: ' + el
      )
      return document.createElement('div')
    }
    return selected
  } else {
    return el
  }
}

el 参数表示挂载的元素,可以传 字符串(如'#app') 或 DOM 对象,如果在浏览器环境中,则调用 query 处理,从 query 函数源码可以看到,如果 el 是字符串,则通过 querySelector 获取到 DOM 对象后返回,否则直接返回

$mount 其实是一个桥接函数,他实际上是调用 query 方法处理 el 后调用 mountComponent 方法,下面看一下 mountComponent 方法,该方法在 src/core/instance/lifecycle.js 文件中

export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el
  if (!vm.$options.render) {
    vm.$options.render = createEmptyVNode
    if (process.env.NODE_ENV !== 'production') {
      /* istanbul ignore if */
      if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
        vm.$options.el || el) {
        warn(
          'You are using the runtime-only build of Vue where the template ' +
          'compiler is not available. Either pre-compile the templates into ' +
          'render functions, or use the compiler-included build.',
          vm
        )
      } else {
        warn(
          'Failed to mount component: template or render function not defined.',
          vm
        )
      }
    }
  }
  callHook(vm, 'beforeMount')

  let updateComponent
  /* istanbul ignore if */
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    updateComponent = () => {
      const name = vm._name
      const id = vm._uid
      const startTag = `vue-perf-start:${id}`
      const endTag = `vue-perf-end:${id}`

      mark(startTag)
      const vnode = vm._render()
      mark(endTag)
      measure(`vue ${name} render`, startTag, endTag)

      mark(startTag)
      vm._update(vnode, hydrating)
      mark(endTag)
      measure(`vue ${name} patch`, startTag, endTag)
    }
  } else {
    updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }
  }

  // we set this to vm._watcher inside the watcher's constructor
  // since the watcher's initial patch may call $forceUpdate (e.g. inside child
  // component's mounted hook), which relies on vm._watcher being already defined
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  hydrating = false

  // manually mounted instance, call mounted on self
  // mounted is called for render-created child components in its inserted hook
  if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')
  }
  return vm
}

可以看到前面有一个判断,如果到了这一步,还没有得到 render 方法,并且 vue 挂载时使用了 template,说明 template 没有正确转换成 render,说明用户引错版本了,应该引入 runtime-compiler 版本,这时会给出一个警告,还有一种情况是既没有 render 也没有 template ,这时又会给出另一个警告

如果一切正常,就会先执行 beforeMount 生命周期,然后组装一个 updateComponent 方法,该方法是用于更新视图的,如果用户开启了 performance 性能检测,则会在 updateComponent 方法中加一些标识

最后实例化一个观察者系统 Watcher,传入 updateComponent,观察者系统用于初始化页面,以及监听到数据更新时,通知 watcher 调用 updateComponent 更新视图

那 vue 是如何更新视图的呢?可以看到 updateComponent 中其实调用了 vm._update() 方法,该方法用于更新视图,该方法参数中的 vm._render() 用于生成 vnode ,所以 vue 更新视图其实涉及到以下三点

下面看一下这三点

Virtual DOM 虚拟节点

由于操作 DOM 是非常耗费性能的,于是 vue 引入了 Virtual DOM ,Virtual DOM 就是使用一个对象来描述 DOM 节点,当我们在 vue 中操作 DOM 时,实际上就是操作对象,操作对象可就比操作 DOM 快多啦,而且操作完成后,vue 会通过 diff 算法得出最小的修改量,最终通过最小的修改更新视图,从而提高性能

vnode class 定义在 src/core/vdom/vnode.js 中

export default class VNode {
  tag: string | void;
  data: VNodeData | void;
  children: ?Array<VNode>;
  text: string | void;
  elm: Node | void;
  ns: string | void;
  context: Component | void; // rendered in this component's scope
  key: string | number | void;
  componentOptions: VNodeComponentOptions | void;
  componentInstance: Component | void; // component instance
  parent: VNode | void; // component placeholder node

  // strictly internal
  raw: boolean; // contains raw HTML? (server only)
  isStatic: boolean; // hoisted static node
  isRootInsert: boolean; // necessary for enter transition check
  isComment: boolean; // empty comment placeholder?
  isCloned: boolean; // is a cloned node?
  isOnce: boolean; // is a v-once node?
  asyncFactory: Function | void; // async component factory function
  asyncMeta: Object | void;
  isAsyncPlaceholder: boolean;
  ssrContext: Object | void;
  fnContext: Component | void; // real context vm for functional nodes
  fnOptions: ?ComponentOptions; // for SSR caching
  fnScopeId: ?string; // functional scope id support

  constructor (
    tag?: string,
    data?: VNodeData,
    children?: ?Array<VNode>,
    text?: string,
    elm?: Node,
    context?: Component,
    componentOptions?: VNodeComponentOptions,
    asyncFactory?: Function
  ) {
    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
  }

  // DEPRECATED: alias for componentInstance for backwards compat.
  /* istanbul ignore next */
  get child (): Component | void {
    return this.componentInstance
  }
}

可以看到 vnode 对象包含许多属性,前面几个是常见的

  • tag:节点标签名
  • data:节点属性
  • children:子节点
  • text:节点文本
  • elm:真实的 DOM

render

render 是用于生成 vnode 的,render 的源码定义在 src/core/instance/render.js

Vue.prototype._render = function (): VNode {
  const vm: Component = this
  const { render, _parentVnode } = vm.$options

  // reset _rendered flag on slots for duplicate slot check
  if (process.env.NODE_ENV !== 'production') {
    for (const key in vm.$slots) {
      // $flow-disable-line
      vm.$slots[key]._rendered = false
    }
  }

  if (_parentVnode) {
    vm.$scopedSlots = _parentVnode.data.scopedSlots || emptyObject
  }

  // 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 {
    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') {
      if (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
      }
    } else {
      vnode = vm._vnode
    }
  }
  // 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 源码中有一行关键代码:调用 render

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

Vue 在调用 render 时传进了 vm.$createElement 方法,Vue 就是通过这个方法创建 vnode 的

由上文可知,render 可能是用户传入的,也可能是编译 template 生成的,对于用户传入的情况,习惯用 h 来接收 $createElement

new Vue({
  el: '#app',
  render: function (h) { // h 接收 $createElement 方法
    return h('h1', 'title')
  }
}

下面看一下 $createElement 方法

$createElement

$createElement 定义在 src/core/instance/render.js

export function initRender (vm: Component) {
  // ...
  // bind the createElement fn to this instance
  // so that we get proper render context inside it.
  // args order: tag, data, children, normalizationType, alwaysNormalize
  // internal version is used by render functions compiled from templates
  vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
  // normalization is always applied for the public version, used in
  // user-written render functions.
  vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
}

可以看到这里定义了两个方法 _c 与 $createElement ,它们都调用了 createElement 方法,只是最后一个参数传参不同

刚刚说到,render 可能是编译生成,也可能是用户手写传入,从注释可以看到 _c 用于处理编译生成的 render,$createElement 用于处理用户手写传入的 render

下面看看这俩方法调用的 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> {
  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 先进行了参数处理,它判断了 data 是不是数组,如果参数 data 是数组,说明用户第二个参数传的是子节点而不是节点属性,这时,重新调整参数位置,这么做的目的是,用户使用 render 时可以在第二个参数传子节点,也可以在第三个参数传子节点,如果熟悉 render 的用法,这部分应该不难理解

// 在第二个参数传子节点
render (createElement) {
  return createElement('div', [
    createElement('span', '内容')
  ])
}
// 在第三个参数传子节点(第二个参数传节点属性)
render (createElement) {
  return createElement('div', {style: {}}, [
    createElement('span', '内容')
  ])
}

对于 alwaysNormalize 这个参数,_c 传的是 false,$createElement 传的是 true,如果为 true ,normalizationType 会被赋值为 ALWAYS_NORMALIZE,normalizationType 从命名就可以知道它决定了(子节点)规范化的方式

关于子节点规范化可在下文 _createElement 源码中看到,下面看下 _createElement

_createElement

createElement 也是一个桥接函数,它先对参数进行处理,然后调用了 _createElement 方法

export function _createElement (
  context: Component,
  tag?: string | Class<Component> | Function | Object,
  data?: VNodeData,
  children?: any,
  normalizationType?: number
): VNode | Array<VNode> {
  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
    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) {
    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)) {
      // platform built-in elements
      vnode = new VNode(
        config.parsePlatformTagName(tag), data, children,
        undefined, undefined, context
      )
    } else if (isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
      // component
      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 会统一处理为数组,这就是子节点规范化

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

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

这两个规范化方法定义在 src/core/vdom/helpers/normalzie-children.js

// The template compiler attempts to minimize the need for normalization by
// statically analyzing the template at compile time.
//
// For plain HTML markup, normalization can be completely skipped because the
// generated render function is guaranteed to return Array<VNode>. There are
// two cases where extra normalization is needed:

// 1. When the children contains components - because a functional component
// may return an Array instead of a single root. In this case, just a simple
// normalization is needed - if any child is an Array, we flatten the whole
// thing with Array.prototype.concat. It is guaranteed to be only 1-level deep
// because functional components already normalize their own children.
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
}

// 2. When the children contains constructs that always generated nested Arrays,
// e.g. <template>, <slot>, v-for, or when the children is provided by user
// with hand-written render functions / JSX. In such cases a full normalization
// is needed to cater to all possible types of children values.
export function normalizeChildren (children: any): ?Array<VNode> {
  return isPrimitive(children)
    ? [createTextVNode(children)]
    : Array.isArray(children)
      ? normalizeArrayChildren(children)
      : undefined
}

编译生成的 render 调用的是 simpleNormalizeChildren ,这个方法直接将 children 拍平,这里我也不太清楚什么时候会调用,我写的测试代码没触发到,全局搜索一下,应该是在函数式组件中使用的,不过 函数式组件 在 vue3.0 已经废弃了,先不管了🤣

用户手写的 render 调用的是 normalizeChildren 这个方法对应两个场景

// 场景1 children 传的是基础类型
render (createElement) {
  return createElement('div', {style: {}}, 'vue')
}

// 场景2 children 传的是数组
render (createElement) {
  return createElement('div', {style: {}}, [
    'vue'
    createElement('span', '数组')
  ])
}

如果 children 传的是基础类型,那么直接创建一个文本vnode 放在数组中,如果 children 传的是数组,则调用 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 = 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
}

这个方法大概是,如果判断到 item 是数组,则递归,这里有个优化操作:如果当前 结果 的最后一个 与 下一个数组第 0 个都是文本类型,那么将这两个合并起来

如果 item 是基础类型则,直接创建文本 vnode,push 进 res,这里也有一个合并文本类型的优化

经过一番循环处理,得到 res 返回出去, res 是 vnode 数组

将 children 规范化后,下面就是创建 vnode 了,它的逻辑是先处理子节点,最后再创建最外层那个

let vnode, ns
if (typeof tag === 'string') {
  let Ctor
  ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
  if (config.isReservedTag(tag)) {
    // platform built-in elements
    vnode = new VNode(
      config.parsePlatformTagName(tag), data, children,
      undefined, undefined, context
    )
  } else if (isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
    // component
    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)
}

上面有说到过 VNode class,创建 vnode 就是实例化 VNode class

这里先判断 tag 是否 string 类型,是则接着判断如果是内置节点就创建一个普通 vnode,如果是组件,则创建一个组件 vnode,否则创建一个未知 tag 的 vnode;如果 tag 是组件类型,则直接通过 createComponent 创建组件 vnode

以上就是 createElement 的主要流程,它最终生成并返回了 vnode,得到 vnode 就可以通过 update 将 vnode 转成真实 dom 从而更新视图了

update

页面初始化 以及 数据更新时会调用 _update ,它将 vnode 转为真实的 DOM 渲染到页面上,_update 定义在 src/core/instance/lifecycle.js

Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
  const vm: Component = this
  const prevEl = vm.$el
  const prevVnode = vm._vnode
  const prevActiveInstance = activeInstance
  activeInstance = vm
  vm._vnode = vnode
  // Vue.prototype.__patch__ is injected in entry points
  // based on the rendering backend used.
  if (!prevVnode) {
    // initial render
    vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
  } else {
    // updates
    vm.$el = vm.__patch__(prevVnode, vnode)
  }
  activeInstance = prevActiveInstance
  // update __vue__ reference
  if (prevEl) {
    prevEl.__vue__ = null
  }
  if (vm.$el) {
    vm.$el.__vue__ = vm
  }
  // if parent is an HOC, update its $el as well
  if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
    vm.$parent.$el = vm.$el
  }
  // updated hook is called by the scheduler to ensure that children are
  // updated in a parent's updated hook.
}

对于首次渲染页面,prevXXX 这些变量肯定是没有的了,所以它会走到

vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)

可以看到第一个参数传入了 挂载节点,第二个参数传入了 render 生成的 vnode,第三个参数是服务端渲染相关的,可以返回上面 _init 的代码看一下,挂载时这个参数根本就没传所以它是 undefined,或者说 false,第四个参数 removeOnly 是 transition-group 使用的,传入了 false

下面看看 patch ,定义在 src/platforms/web/runtime/index.js

// install platform patch function
Vue.prototype.__patch__ = inBrowser ? patch : noop

在浏览器环境,它指向了 patch ,patch 定义在 src/platforms/web/runtime/patch.js

import * as nodeOps from 'web/runtime/node-ops'
import { createPatchFunction } from 'core/vdom/patch'
import baseModules from 'core/vdom/modules/index'
import platformModules from 'web/runtime/modules/index'

// the directive module should be applied last, after all
// built-in modules have been applied.
const modules = platformModules.concat(baseModules)

export const patch: Function = createPatchFunction({ nodeOps, modules })

可以看到 patch 是通过 createPatchFunction 生成的

这里传入一个对象,包含 nodeOps 和 modules

nodeOps 是操作节点的方法,例如创建节点,插入节点等

modules 是处理属性的方法,例如创建 class、更新 class 、创建 ref 、 销毁 ref 等操作,这些方法都是时机命名( create activate update remove destroy ),这样命名是为了在 createPatchFunction 中与 hook 进行匹配提取,比如把 class 和 style 等所有属性的 create 方法提取出来,然后在 create 阶段执行这些方法,就可以把所有属性创建出来了,具体看下面 createPatchFunction 的代码

这里之所以把 nodeOps 和 modules 抽出来,作为参数传入,原因是每个平台处理节点的方法是不同的,比如当判断为 web 环境,那么就传入 web 环境相关的节点处理方法,如果是 weex 环境,那么就传 weex 的,这样做的好处是,在函数外就把差异抹平,在函数中能省去许多判断平台的代码,平时写代码可以学习这种技巧

createPatchFunction 定义在 src/core/vdom/patch.js

const hooks = ['create', 'activate', 'update', 'remove', 'destroy']

export function createPatchFunction (backend) {
  let i, j
  const cbs = {}

  const { modules, nodeOps } = backend

  for (i = 0; i < hooks.length; ++i) {
    cbs[hooks[i]] = []
    for (j = 0; j < modules.length; ++j) {
      if (isDef(modules[j][hooks[i]])) {
        cbs[hooks[i]].push(modules[j][hooks[i]])
      }
    }
  }

  // ... 中间是一些辅助方法,省略

  return function patch (oldVnode, vnode, hydrating, removeOnly) {
    if (isUndef(vnode)) {
      if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
      return
    }

    let isInitialPatch = false
    const insertedVnodeQueue = []

    if (isUndef(oldVnode)) {
      // empty mount (likely as component), create new root element
      isInitialPatch = true
      createElm(vnode, insertedVnodeQueue)
    } else {
      const isRealElement = isDef(oldVnode.nodeType)
      if (!isRealElement && sameVnode(oldVnode, vnode)) {
        // patch existing root node
        patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly)
      } else {
        if (isRealElement) {
          // mounting to a real element
          // check if this is server-rendered content and if we can perform
          // a successful hydration.
          if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
            oldVnode.removeAttribute(SSR_ATTR)
            hydrating = true
          }
          if (isTrue(hydrating)) {
            if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
              invokeInsertHook(vnode, insertedVnodeQueue, true)
              return oldVnode
            } else if (process.env.NODE_ENV !== 'production') {
              warn(
                'The client-side rendered virtual DOM tree is not matching ' +
                'server-rendered content. This is likely caused by incorrect ' +
                'HTML markup, for example nesting block-level elements inside ' +
                '<p>, or missing <tbody>. Bailing hydration and performing ' +
                'full client-side render.'
              )
            }
          }
          // either not server-rendered, or hydration failed.
          // create an empty node and replace it
          oldVnode = emptyNodeAt(oldVnode)
        }

        // replacing existing element
        const oldElm = oldVnode.elm
        const parentElm = nodeOps.parentNode(oldElm)

        // create new node
        createElm(
          vnode,
          insertedVnodeQueue,
          // extremely rare edge case: do not insert if old element is in a
          // leaving transition. Only happens when combining transition +
          // keep-alive + HOCs. (#4590)
          oldElm._leaveCb ? null : parentElm,
          nodeOps.nextSibling(oldElm)
        )

        // update parent placeholder node element, recursively
        if (isDef(vnode.parent)) {
          let ancestor = vnode.parent
          const patchable = isPatchable(vnode)
          while (ancestor) {
            for (let i = 0; i < cbs.destroy.length; ++i) {
              cbs.destroy[i](ancestor)
            }
            ancestor.elm = vnode.elm
            if (patchable) {
              for (let i = 0; i < cbs.create.length; ++i) {
                cbs.create[i](emptyNode, ancestor)
              }
              // #6513
              // invoke insert hooks that may have been merged by create hooks.
              // e.g. for directives that uses the "inserted" hook.
              const insert = ancestor.data.hook.insert
              if (insert.merged) {
                // start at index 1 to avoid re-invoking component mounted hook
                for (let i = 1; i < insert.fns.length; i++) {
                  insert.fns[i]()
                }
              }
            } else {
              registerRef(ancestor)
            }
            ancestor = ancestor.parent
          }
        }

        // destroy old node
        if (isDef(parentElm)) {
          removeVnodes(parentElm, [oldVnode], 0, 0)
        } else if (isDef(oldVnode.tag)) {
          invokeDestroyHook(oldVnode)
        }
      }
    }

    invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
    return vnode.elm
  }
}

createPatchFunction 前面是对 modules 的提取,最后返回 patch 方法

当调用 patch 时,由于 oldVnode 传入的是 挂载节点,所以会走到这里

if (isRealElement) {
  const isRealElement = isDef(oldVnode.nodeType)
  // mounting to a real element
  // check if this is server-rendered content and if we can perform
  // a successful hydration.
  if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
    oldVnode.removeAttribute(SSR_ATTR)
    hydrating = true
  }
  if (isTrue(hydrating)) {
    if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
      invokeInsertHook(vnode, insertedVnodeQueue, true)
      return oldVnode
    } else if (process.env.NODE_ENV !== 'production') {
      warn(
        'The client-side rendered virtual DOM tree is not matching ' +
        'server-rendered content. This is likely caused by incorrect ' +
        'HTML markup, for example nesting block-level elements inside ' +
        '<p>, or missing <tbody>. Bailing hydration and performing ' +
        'full client-side render.'
      )
    }
  }
  // either not server-rendered, or hydration failed.
  // create an empty node and replace it
  oldVnode = emptyNodeAt(oldVnode)
}

由于挂载节点就是一个真实 DOM 节点,所以 isRealElement 为 true,对于服务端渲染,会进到两个 if 里,最后面是调用了 emptyNodeAt 方法,这个方法将挂载节点转成了 vnode

function emptyNodeAt (elm) {
  return new VNode(nodeOps.tagName(elm).toLowerCase(), {}, [], undefined, elm)
}

接下来调用了 createElm

// replacing existing element
const oldElm = oldVnode.elm
const parentElm = nodeOps.parentNode(oldElm)

// create new node
createElm(
 vnode,
 insertedVnodeQueue,
 // extremely rare edge case: do not insert if old element is in a
 // leaving transition. Only happens when combining transition +
 // keep-alive + HOCs. (#4590)
 oldElm._leaveCb ? null : parentElm,
 nodeOps.nextSibling(oldElm)
)

第一个参数传入了 vnode ,第二个参数 vnode 队列传入了空数组,第三个参数是父元素,第四个参数是插入时的参考元素是挂载节点的兄弟节点

createElm 创建并插入了真实 DOM,下面看一下

function createElm (
  vnode,
  insertedVnodeQueue,
  parentElm,
  refElm,
  nested,
  ownerArray,
  index
) {
  if (isDef(vnode.elm) && isDef(ownerArray)) {
    // This vnode was used in a previous render!
    // now it's used as a new node, overwriting its elm would cause
    // potential patch errors down the road when it's used as an insertion
    // reference node. Instead, we clone the node on-demand before creating
    // associated DOM element for it.
    vnode = ownerArray[index] = cloneVNode(vnode)
  }

  vnode.isRootInsert = !nested // for transition enter check
  if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
    return
  }

  const data = vnode.data
  const children = vnode.children
  const tag = vnode.tag
  if (isDef(tag)) {
    if (process.env.NODE_ENV !== 'production') {
      if (data && data.pre) {
        creatingElmInVPre++
      }
      if (isUnknownElement(vnode, creatingElmInVPre)) {
        warn(
          'Unknown custom element: <' + tag + '> - did you ' +
          'register the component correctly? For recursive components, ' +
          'make sure to provide the "name" option.',
          vnode.context
        )
      }
    }

    vnode.elm = vnode.ns
      ? nodeOps.createElementNS(vnode.ns, tag)
      : nodeOps.createElement(tag, vnode)
    setScope(vnode)

    /* istanbul ignore if */
    if (__WEEX__) {
      // ...
    } else {
      createChildren(vnode, children, insertedVnodeQueue)
      if (isDef(data)) {
        invokeCreateHooks(vnode, insertedVnodeQueue)
      }
      insert(parentElm, vnode.elm, refElm)
    }

    if (process.env.NODE_ENV !== 'production' && data && data.pre) {
      creatingElmInVPre--
    }
  } else if (isTrue(vnode.isComment)) {
    vnode.elm = nodeOps.createComment(vnode.text)
    insert(parentElm, vnode.elm, refElm)
  } else {
    vnode.elm = nodeOps.createTextNode(vnode.text)
    insert(parentElm, vnode.elm, refElm)
  }
}

进入这个方法后,由于 tag 是存在的,所以走到了下面部分代码

if (isDef(tag)) {
 if (process.env.NODE_ENV !== 'production') {
   if (data && data.pre) {
     creatingElmInVPre++
   }
   if (isUnknownElement(vnode, creatingElmInVPre)) {
     warn(
       'Unknown custom element: <' + tag + '> - did you ' +
       'register the component correctly? For recursive components, ' +
       'make sure to provide the "name" option.',
       vnode.context
     )
   }
 }

 vnode.elm = vnode.ns
   ? nodeOps.createElementNS(vnode.ns, tag)
   : nodeOps.createElement(tag, vnode)
 setScope(vnode)

 /* istanbul ignore if */
 if (__WEEX__) {
   // ...
 } else {
   createChildren(vnode, children, insertedVnodeQueue)
   if (isDef(data)) {
     invokeCreateHooks(vnode, insertedVnodeQueue)
   }
   insert(parentElm, vnode.elm, refElm)
 }

 if (process.env.NODE_ENV !== 'production' && data && data.pre) {
   creatingElmInVPre--
 }
} else if (isTrue(vnode.isComment)) {
 vnode.elm = nodeOps.createComment(vnode.text)
 insert(parentElm, vnode.elm, refElm)
} else {
 vnode.elm = nodeOps.createTextNode(vnode.text)
 insert(parentElm, vnode.elm, refElm)
}

可以看到这里对 tag 调用 isUnknownElement 方法进行了检查,如果这是一个未知的标签,那么会给出警告,这个警告经常能看到,平时忘记注册组件时,就会出现,因为未注册对组件对 vue 来说就是 unknow element

接下来调用了 nodeOps.createElement 创建了真实的 DOM 节点,并存在 vnode 的 elm 属性中,这个 elm 在 vnode class 上可以看到,这是用于存 vnode 的真实 DOM 的属性

可以看一下 nodeOps.createElement 这个方法,其实就是调用了 document.createElement 创建 DOM 节点

export function createElement (tagName: string, vnode: VNode): Element {
  const elm = document.createElement(tagName)
  if (tagName !== 'select') {
    return elm
  }
  // false or null will remove the attribute but undefined will not
  if (vnode.data && vnode.data.attrs && vnode.data.attrs.multiple !== undefined) {
    elm.setAttribute('multiple', 'multiple')
  }
  return elm
}

接下对环境进行了判断,浏览器环境会走到 else

if (__WEEX__) {
  // ...
} else {
  createChildren(vnode, children, insertedVnodeQueue)
  if (isDef(data)) {
    invokeCreateHooks(vnode, insertedVnodeQueue)
  }
  insert(parentElm, vnode.elm, refElm)
}

可以看到,这里首先调用了 createChildren ,这个是用于创建 vnode 的子节点,它其实就是循环 children,然后递归调用 createElm 创建子元素

function createChildren (vnode, children, insertedVnodeQueue) {
 if (Array.isArray(children)) {
   if (process.env.NODE_ENV !== 'production') {
     checkDuplicateKeys(children)
   }
   for (let i = 0; i < children.length; ++i) {
     createElm(children[i], insertedVnodeQueue, vnode.elm, null, true, children, i)
   }
 } else if (isPrimitive(vnode.text)) {
   nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(String(vnode.text)))
 }
}

之后如果存在属性配置,则调用 invokeCreateHooks 执行创建属性的钩子函数,还记得刚刚对 modules 进行了提取吗,这里就是循环调用提取出来的 create 方法,进行各种属性的创建

function invokeCreateHooks (vnode, insertedVnodeQueue) {
  for (let i = 0; i < cbs.create.length; ++i) {
    cbs.create[i](emptyNode, vnode)
  }
  i = vnode.data.hook // Reuse variable
  if (isDef(i)) {
    if (isDef(i.create)) i.create(emptyNode, vnode)
    if (isDef(i.insert)) insertedVnodeQueue.push(vnode)
  }
}

最后调用了 insert 方法,将创建好的 DOM 元素插入文档中

function insert (parent, elm, ref) {
 if (isDef(parent)) {
   if (isDef(ref)) {
     if (nodeOps.parentNode(ref) === parent) {
       nodeOps.insertBefore(parent, elm, ref)
     }
   } else {
     nodeOps.appendChild(parent, elm)
   }
 }
}

如果存在参照节点,那么在插入到参照节点的前面,否则插入到父元素后面。

可以看到初始化时不涉及 patch 相对来说算简单了

到此, vue 将页面初始化好了

总结

vue 初始化的整个流程,是从 初始化 开始的,这里 vue 将传入的 option 与 Vue 静态属性 option 合并,并执行了一系列的 init 方法

初始化之后进行挂载,如果使用了 template ,则会将 template 转为 render

vue 其实只认 render,不管是编译生成的,还是用户手写的,有了 render ,就可以通过 _render 方法生成 vnode

创建好 vnode 后通过 update 方法将 vnode 转成真实的 DOM 并插入到 html 文档中