从vue2源码看vue生命周期--初始化阶段(学习笔记)

763 阅读3分钟

生命周期的讲解可以参考这篇文章

上篇文章更偏向于解释vue生命周期在不同的阶段都做了什么,但是他又是如何实现的我们不得而知,vue生命周期主要来说分为四个阶段,初始化阶段、模板编译阶段、挂载阶段和销毁阶段,从源码的角度来深入学习一下vue生命周期初始化阶段所做的工作和内部原理。

初始化阶段

从生命周期流程图中我们可以看到,初始化阶段所做的工作也可大致分为两部分:第一部分是new Vue(),也就是创建一个Vue实例;第二部分是为创建好的Vue实例初始化一些事件、属性、响应式数据等。

new Vue()

new 关键字在 JS中表示从一个类中实例化出一个对象来,由此, Vue 实际上是一个类,所以new Vue()实际上是执行了Vue类的构造函数。

image.png

在源码的src/core/instance/index.js文件里看到了Vue类的定义

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)  // 核心代码
}

我们可以看出在这段代码中的核心代码其实就是this._init(options),调用原型上的_init()方法并将option传入。而_init方法又是通过执行initMixin(Vue)得到的,接下来在src/core/instance/init.js文件下看看initMixin函数:

image.png

这是全部的initMixin方法的代码:

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)
    }
  }
}

为了更加直观,我们把一些不太重要的代码剔除然后留下核心代码:

export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
    vm._isVue = true
    vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
  )
    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')
    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }
}

可以看到,在 initMixin 函数内部就只干了一件事,那就是给 Vue 类的原型上绑定 _init 方法,同时 _init 方法的定义也在该函数内部。现在我们知道了,new Vue() 会执行 Vue 类的构造函数,构造函数内部会执行 _init 方法,所以 new Vue() 的作用其实就是 _init 方法的作用,那么我们仔细看看_init 方法都干了哪些事情:

  • 首先,把 Vue 实例赋值给变量 vm ,并且把用户传递的 options 选项与当前构造函数的 options 属性及其父级构造函数的 options 属性进行合并(关于属性如何合并的问题下面会介绍),得到一个新的 options 选项赋值给 $options 属性,并将 $options 属性挂载到 Vue 实例上。
vm.$options = mergeOptions( 
    resolveConstructorOptions(vm.constructor)
    options || {}
    vm
)
  • 然后通过调用一些初始化函数来为 Vue 实例初始化一些属性,事件,响应式数据。
initLifecycle(vm)             // 初始化生命周期
initEvents(vm)                // 初始化事件
initRender(vm)                // 初始化渲染
callHook(vm, 'beforeCreate')  // 调用生命周期钩子函数
initInjections(vm)            //初始化injections
initState(vm)                 // 初始化props,methods,data,computed,watch
initProvide(vm)               // 初始化 provide
callHook(vm, 'created')       // 调用生命周期钩子函数
  • 在所有的初始化工作完成后,判断是否传入了 el选项,如果传入了则调用 $mount 函数进入模板编译和挂载阶段,如果没有就不进入下一个生命周期需要用户手动执行 vm.$mount 方法才进入下一个生命周期阶段。

由上面的的new Vue()阶段可以看出它完成了 Vue 的整个初始化阶段,接下来再来看看 new Vue() 里调用的那几个初始化函数。

属性合并

_init 方法里首先会调用 mergeOptions 函数来进行属性合并。

vm.$options = mergeOptions( 
    resolveConstructorOptions(vm.constructor)
    options || {}
    vm
)

它实际上就是把 resolveConstructorOptions(vm.constructor) 的返回值和 options 做合并,下面就是resolveConstructorOptions()方法:

function resolveConstructorOptions (Ctor) {
  var options = Ctor.options;
  if (Ctor.super) {
    var superOptions = resolveConstructorOptions(Ctor.super);
    var cachedSuperOptions = Ctor.superOptions;
    if (superOptions !== cachedSuperOptions) {
      // super option changed,
      // need to resolve new options.
      Ctor.superOptions = superOptions;
      // check if there are any late-modified/attached options (#4976)
      var modifiedOptions = resolveModifiedOptions(Ctor);
      // update base extend options
      if (modifiedOptions) {
        extend(Ctor.extendOptions, modifiedOptions);
      }
      options = Ctor.options = mergeOptions(superOptions, Ctor.extendOptions);
      if (options.name) {
        options.components[options.name] = Ctor;
      }
    }
  }
  return options
}

在 initGlobalAPI(Vue) 的时候定义了 Vue.options,代码在 src/core/global-api/index.js 中:

Vue.options = Object.create(null)
  ASSET_TYPES.forEach(type => {
  Vue.options[type + 's'] = Object.create(null)
})

首先通过 Vue.options = Object.create(null) 创建一个空对象,然后遍历 ASSET_TYPESASSET_TYPES 的定义在 src/shared/constants.js 中:

export const ASSET_TYPES = [
  'component',
  'directive',
  'filter'
]

遍历 ASSET_TYPES 后的代码相当于:

Vue.options.components = {}
Vue.options.directives = {}
Vue.options.filters = {}

最后通过 extend(Vue.options.components, builtInComponents) 把一些内置组件扩展到 Vue.options.components 上,Vue 的内置组件目前 有<keep-alive><transition> 和<transition-group> 组件,这也就是为什么我们在其它组件中使用这些组件不需要注册的原因。

那么回到 mergeOptions 这个函数,它的定义在 src/core/util/options.js 中,可以看出,mergeOptions函数的主要功能是把 parent 和 child 这两个对象根据一些合并策略,合并成一个新对象并返回。

/**
 * Merge two option objects into a new one.
 * Core utility used in both instantiation and inheritance.
 */
 
export function mergeOptions (
  parent: Object,
  child: Object,
  vm?: Component
): Object {
  if (process.env.NODE_ENV !== 'production') {
    checkComponents(child)
  }

  if (typeof child === 'function') {
    child = child.options
  }

  normalizeProps(child, vm)
  normalizeInject(child, vm)
  normalizeDirectives(child)

  // Apply extends and mixins on the child options,
  // but only if it is a raw options object that isn't
  // the result of another mergeOptions call.
  // Only merged options has the _base property.
  if (!child._base) {                        // 递归把 `extends` 和 `mixins` 合并到 `parent` 上
    if (child.extends) {
      parent = mergeOptions(parent, child.extends, vm)
    }
    if (child.mixins) {
      for (let i = 0, l = child.mixins.length; i < l; i++) {
        parent = mergeOptions(parent, child.mixins[i], vm)
      }
    }
  }

  const options = {}       // 创建一个空对象options,遍历 parent,把 parent 中的每一项通过调用 mergeField 函数合并到空对象options里,
  let key
  for (key in parent) {
    mergeField(key)
  }
  for (key in child) {       //  接着再遍历 child,把存在于 child 里但又不在 parent 中的属性继续调用 mergeField 函数合并到空对象options里
    if (!hasOwn(parent, key)) {
      mergeField(key)
    }
  }
  function mergeField (key) {
    const strat = strats[key] || defaultStrat
    options[key] = strat(parent[key], child[key], vm, key)
  }
  return options           // options就是最终合并后得到的结果
}

首先递归把 extends 和 mixins 合并到 parent 上,然后创建一个空对象options,遍历 parent,把parent中的每一项通过调用 mergeField函数合并到空对象options里,接着再遍历 child,把存在于child里但又不在 parent中 的属性继续调用 mergeField函数合并到空对象options里,最后,options就是最终合并后得到的结果,将其返回。


这里值得一提的是 mergeField 函数,它不是简单的把属性从一个对象里复制到另外一个对象里,而是根据被合并的不同的选项有着不同的合并策略。例如,对于datadata的合并策略,即该文件中的strats.data函数;对于watchwatch的合并策略,即该文件中的strats.watch函数等等。这就是设计模式中非常典型的策略模式


callHook函数触发钩子函数

函数的源码位于src/core/instance/lifecycle.js 中:

export function callHook (vm: Component, hook: string) {
  // #7573 disable dep collection when invoking lifecycle hooks
  pushTarget()
  const handlers = vm.$options[hook]
  const info = `${hook} hook`
  if (handlers) {
    for (let i = 0, j = handlers.length; i < j; i++) {
      invokeWithErrorHandling(handlers[i], vm, null, vm, info)
    }
  }
  if (vm._hasHookEvent) {
    vm.$emit('hook:' + hook)
  }
  popTarget()
}

首先从实例的$options中获取到需要触发的钩子名称所对应的钩子函数数组handlers,我们说过,每个生命周期钩子名称都对应了一个钩子函数数组。然后遍历该数组,将数组中的每个钩子函数都执行一遍。

注:文章部分摘自vue源码学习社区,原文十分详细,笔者只是跟随思路阅读并手打一遍留下印象,方便以后回顾,感兴趣的同学可到原文中细学。