Vue初始化都干了啥 -- 合并配置

56 阅读1分钟

初识创建Vue实例流程中,对创建一个Vue实例已经有了一个大概的了解。由于组件实例new Vue实例的创建会走不到不同的分支,所以这一节会忽略组件实例初始化的部分,在patch阶段再回过头来讲解组件实例的创建过程。

准备阶段

// html片段 <div id="app"></div>
// index.js
new Vue({
    el: '#app'
})

现在我们创建一个Vue实例,在上一节的基础上细化分析。我们已经知道Vue构造函数接受一个options对象,并执行_init(options)方法初始化对象。其中第一步就是先合并配置。

合并配置

// src/core/instance/index.js
if (options && options._isComponent) {
    initInternalComponent(vm, options)
} else {
    vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
    )
}

合并配置的逻辑有两个分支,这里可以明确的告诉大家,组件实例的options上才会有_isComponent属性,我们的例子并没有传递这个属性。所以这里走到了下面的分支。调用mergeOptions函数合并配置,它接受三个参数,resolveConstructorOptions函数的返回值,传入的options和当前实例vm。那这里就只有一个参数不明确就是resolveConstructorOptions的返回值。那么它返回什么呢?

resolveConstructorOptions

export function resolveConstructorOptions (Ctor: Class<Component>) {
    let options = Ctor.options
    if (Ctor.super) {
        // 递归的返回父类的options
        const superOptions = resolveConstructorOptions(Ctor.super)
        // 缓存的父类options
        const cachedSuperOptions = Ctor.superOptions
        // 如果发生了改变,如使用Mixin改变了Vue的options
        if (superOptions !== cachedSuperOptions) {
            Ctor.superOptions = superOptions
            const modifiedOptions = resolveModifiedOptions(Ctor)
            if (modifiedOptions) {
                extend(Ctor.extendOptions, modifiedOptions)
            }
            // 更新修改了的配置
            options = Ctor.options = mergeOptions(superOptions, Ctor.extendOptions)
            if (options.name) {
                options.components[options.name] = Ctor
            }
        }
    }
    return options
}

我们使用的是new Vue的方式,所以构造函数上并不存在super属性,也不会走到if分支。resolveConstructorOptions最终就是返回构造函数上的options。Vue构造函数的options认识Vue构造函数时提到过,如下

    options: {
        components: {
            KeepAlive,
            transition,
            transitionGroup
        },
        directives: {
            model,
            show
        },
        filters: {},
        _base: Vue
    },

现在所有的参数,已经知道了,下面就开始合并属性

mergeOptions

// src/core/util/options.js
// 空对象
const strats = config.optionMergeStrategies
strats.data = function() {}
export function mergeOptions (
  parent: Object,
  child: Object,
  vm?: Component
): Object {
  if (typeof child === 'function') {
    child = child.options
  }
  // 格式化props
  // 所有写法转化为一个对象,并且每个属性都是一个包含type属性的对象
  normalizeProps(child, vm)
  // 所有写法转化为一个对象,并且每个属性都是一个包含from属性的对象
  normalizeInject(child, vm)
  // 格式化指令
  normalizeDirectives(child)
  // 只在Vue实例,不是其他mergeOptions结果的对象上合并extends和mixins
  if (!child._base) {
    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 = {}
  let key
  for (key in parent) {
    mergeField(key)
  }
  for (key in child) {
    if (!hasOwn(parent, key)) {
      mergeField(key)
    }
  }
  // strats合并策略对象,根据不同的配置完成不同的合并策略
  function mergeField (key) {
    const strat = strats[key] || defaultStrat
    options[key] = strat(parent[key], child[key], vm, key)
  }
  return options
}

首先需要了解的是,在执行mergeOptions之前,会初始化一个空对象strats,它保存了每种属性(如data, computed)对应的合并方法。

合并流程

  1. 格式化props,inject,directive,最终都会转会为对象
  2. 递归调用mergeOptions合并child(传入的options)中的extends和mixins
  3. 不同属性根据不同的合并规则合并

如果格式化和各个属性的合并规则全部都贴一遍代码,工作量太大,同时意义也不大。格式化的代码我会考虑另外写一篇总结性的文章来讨论,而合并的规则使用流程图的方式

mergeOptions-1.png

需要注意的是dataprovide因为其特殊性,合并返回的并不是一个对象,而是一个方法,合并发生在初始化的过程。合并的规则可以理解成递归的合并,同名的属性将会覆盖默认值(mixin等混入的值)。watch的合并规则和生命周期钩子函数类似,最后都会被转化为数组,并且优先执行默认的(mixin等混入的值)

初始化生命周期

export function initLifecycle (vm: Component) {
  const options = vm.$options
  // locate first non-abstract parent
  let parent = options.parent
  // 找到第一个非抽象组件作为父组件,并向父组件中添加本实例
  // 如果是new构造的实例,parent就是为空
  if (parent && !options.abstract) {
    while (parent.$options.abstract && parent.$parent) {
      parent = parent.$parent
    }
    parent.$children.push(vm)
  }
​
  vm.$parent = parent
  vm.$root = parent ? parent.$root : vm
​
  vm.$children = []
  vm.$refs = {}
  // 生命周期相关的属性
  vm._watcher = null
  vm._inactive = null
  vm._directInactive = false
  vm._isMounted = false
  vm._isDestroyed = false
  vm._isBeingDestroyed = false
}

合并生命周期,就相对的简单一些,定义一些属性,记录实例的状态,如是否挂载,父组件实例,子组件实例等等。需要注意的是,在记录父组件实例的时候会跳过抽象组件,如keep-alive

如何调用生命周期钩子函数

在初始化过程中,通过callHook函数调用了beforeCreatecreated钩子函数,那么它是怎么执行我们编写的钩子函数?

// src/core/instance/lifecycle.js
export function callHook (vm: Component, hook: string) {
    // 更新全局的target置空
    // 防止钩子函数执行过程修改属性,造成死循环(个人理解,有待考证)
  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)
    }
  }
    // 父组件在标签上使用@hooks="xxx"的方式
  if (vm._hasHookEvent) {
    vm.$emit('hook:' + hook)
  }
  popTarget()
}
// src/core/util/error.js
export function invokeWithErrorHandling (
  handler: Function,
  context: any,
  args: null | any[],
  vm: any,
  info: string
) {
  let res
  try {
    res = args ? handler.apply(context, args) : handler.call(context)
      // 如果是promise
    if (res && !res._isVue && isPromise(res) && !res._handled) {
      res.catch(e => handleError(e, vm, info + ` (Promise/async)`))
      res._handled = true
    }
  } catch (e) {
    handleError(e, vm, info)
  }
  return res
}

在合并配置阶段,生命周期钩子函数已经被转化为数组,在callHook中,根据传入的生命周期钩子函数名,循环执行各个生命周期函数。

结尾

eventsRenderinjectprovide这部分在使用new Vue涉及的并不多,所以打算在创建组件实例的时候再回过头来看。下一篇来讨论状态(initState)的初始化。