vuejs源码解剖 — Vue初始化之合并配置

1,517 阅读14分钟

Vue初始化之合并配置

构造函数Vue在生产环境的第一步就是执行原型链上的_init方法。该方法是在initMixin方法中定义,其中options就是我们调用Vue构造函数的时候传过来的,源代码位置:/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++

首先声明vm常量指向当前Vue实例,然后给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)
}

接下来是性能测试。首先声明了两个变量startTagendTag。如果是在非生产环境下,且config.performancemarktrue的情况下则执行性能追踪。

config.performance来自于core/config.js中的配置。在Vue官方文档中,我们看到可以对config进行修改配置。例如Vue.config.performance = true的时候,非生产环境主要对以下功能进行追踪:

  • 1、组件初始化(component init)
  • 2、编译(compile),将模板(template)编译成渲染函数
  • 3、渲染(render),其实就是渲染函数的性能,或者说渲染函数执行且生成虚拟DOM(vnode)的性能
  • 4、打补丁(patch),将虚拟DOM渲染为真实DOM的性能

    mark这里就不赘述了,在工具方法篇中已做介绍

// 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 {
  //暂时忽略
}

首先在 Vue 实例上添加 _isVue 属性,并设置其值为 true。目的是用来标识一个对象是 Vue 实例,即如果发现一个对象拥有 _isVue 属性并且其值为 true,那么就代表该对象是 Vue 实例,这样可以避免该对象被响应系统观测。
接下来是一个if...else分支。即如果选项上带有_isComponent内部选项,则表示这是已经初始过的组件,这里进行了一个优化策略(暂不做详细介绍,后面会再次讲到)。

合并选项都做了些啥

接下来就是我们本篇文章的核心:选项合并策略。不少人对选项的合并往往嗤之以鼻,认为这不是响应式数据的核心,以为仅仅是两个对象合并成一个对象而已,没必要花太多心思去研究,所以不去深究。其实大错特错,深入研究后你才会发现不仅能从中学到不少东西,而且还可以深入了解每个组件的数据结构,这是理解Vue源码的基础。

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

选项合并的目的就是将Vue默认配置与用户自定义的options进行合并后返回一个新的Object,并赋值给实例vm的属性$options。本质是将两个对象合并为一个新对象。

合并主要是mergeOptions函数做的事情,它接受三个参数:

  • 1、resolveConstructorOption函数返回的Vue默认配置
  • 2、用户自定义的默认配置。若无则传空对象
  • 3、当前实例本身

先来看下resolveConstructorOption函数,它的位置也在:core/instance/index.js

export function resolveConstructorOptions (Ctor: Class<Component>) {
  let options = Ctor.options
  if (Ctor.super) {
    //暂时忽略
  }
  return options
}

resolveConstructorOption函数只接受一个参数Ctor,那么这个Ctor是啥呢?在实际调用的时候传的唯一参数是vm.constructor,正常情况下(注意是一般正常理解的情况下)这个值指向的其实就是Vue构造函数本身,举个例子:

function testFn(){
  this.a = 1;
}
var vm = new testFn();
console.log(vm.constructor === testFn);  // true

所以let options = Ctor.options其实相当于let options = Vue.options。鉴于读者是刚看源码,对Vue.extend还不熟悉,为了便于理解,我们可以暂时这么理解:resolveConstructorOptions(vm.constructor)其实就是Vue.options。等整体看透之后再回头重看的时候,就能理解了。

不过这里还是会先简单介绍一下resolveConstructorOptions函数中if语句主要是干嘛的。

if (Ctor.super) {
  const superOptions = resolveConstructorOptions(Ctor.super)
  const 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)
    const 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
    }
  }
}

我们看到if语句的判断条件是Ctor.supersuper是子类才有的属性。举个例子:

var sub = Vue.extend();
var vm = new sub();
console.log(vm.constructor);  // sub
console.log(sub.super);  // Vue

以上例子也充分说明了,实例vmconstructor指向的不一定是Vue,这种情况指的是sub函数。因是继承而来的,所以sub函数有super属性。

总结:目前只需要知道resolveConstructorOptions返回Vue.options即可,其中的if(Ctor.super)Vue.extend()方法有关,等到后面遇到的时候再重点分析。

mergeOptions

接下来我们重点看下mergeOptions方法,它接受三个参数。
第一个参数暂时认为是Vue.options

Vue.options = {
  components:{
    keepAlive,
    transition,
    transitinGroup
  },
  directives:{
    model,
    show
  },
  filters:Object.create(null),
  _base:Vue
}

第二个参数是我们调用new Vue的时候传的参数,例如:

{
  el:'#app',
  data(){
    return {
      name:'wang'
    }
  },
  methods:{
    init(){

    }
  }
}

第三个参数就是当前实例本身vm。所以我们改下mergeOptions方法,其实就相当于:

vm.$options = mergeOptions(
  {
    components:{
      keepAlive,
      transition,
      transitionGroup
    },
    directives:{
      model,
      show
    },
    filters:Object.create(null),
    _base:Vue
  },
  {
    el:'#app',
    data(){
      return {
        name:'wang'
      }
    },
    methods:{
      init(){

      }
    }
  },
  vm
)

接下来我们详细看下mergeOptions源代码。位置:core/util/options.js

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
  }
  //剩余代码暂时忽略
}

首先在非生产环境下会使用checkComponents方法检测我们自定义配置中的组件名称命名是否规范。为何要在非生产环境检测呢?因为在非生产环境检测规范后,我们不大可能在build生产的时候再去修改代码,也就是说生产环境没必要再次检测,以此来达到节约性能的目的。类似的情况process.env.NODE_ENV !== 'production'以后会有很多,就不再一一赘述。检测代码如下:

function checkComponents (options: Object) {
  for (const key in options.components) {
    validateComponentName(key)
  }
}
export function validateComponentName (name: string) {
  if (!new RegExp(`^[a-zA-Z][\\-\\.0-9_${unicodeRegExp.source}]*$`).test(name)) {
    warn(
      'Invalid component name: "' + name + '". Component names ' +
      'should conform to valid custom element name in html5 specification.'
    )
  }
  if (isBuiltInTag(name) || config.isReservedTag(name)) {
    warn(
      'Do not use built-in or reserved HTML elements as component ' +
      'id: ' + name
    )
  }
}

通过以上代码可以看出,检测的原理就是遍历我们传递选项的components属性。若满足以下条件,则打印警告:

  • 1、满足正则表达式^[a-zA-Z][\\-\\.0-9_${unicodeRegExp.source}]*$
  • 2、isBuiltInTag(name) || config.isReservedTag(name)之一成立的情况下

说人话?好的好的
其实第一条就是限定组件的命名规则由普通的字符和中横线(-)组成,且必须以字母开头。
第二条是检测你的组件名称不能与内置标签(slotcomponent)冲突,也不能是内置标签。

说了这么多,其实就是保证在合并之前你的组件命名合理合法,作者为了防止开发者犯规,也是操碎了心呐。

允许我们传递的参数是一个函数

接下来的这段代码打破了我们上面说的:我们传递的合并参数是一个对象。其实它也可以是一个函数,这里增加了一个判断,若是函数,则child重新指向它的静态属性child.options

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

什么场景下会遇到这种情况呢?其实还是跟Vue.extend()函数有关,这个在后面也会详细讲解。

规范化props、inject、directives

normalizeProps

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

这三个函数是用来规范选项,方便后面合并而做的处理。为什么要这么做呢?以props为例,我们知道Vue允许以下多种写法:

//写法一
const yourComponents = {
  props:['yourData']
}
//写法二
const yourComponents = {
  props:{
    yourData:{
      type:Number,
      default:1
    }
  }
}
//写法三
const yourComponents = {
  props:{
    yourData:{
      type:Number
    }
  }
}
//写法四
const yourComponents = {
  props:{
    yourData:Number
  }
}

这个给开发者提供了非常便利的选择,可以根据自己的习惯任性的写逻辑。但凡事都有两面性,开发者爽了,源码作者就要写更多方法来适应。也就是在真正合并之前,将开发者写的多种格式统一规范,方便后面合并。接下来以normalizeProps为例:

function normalizeProps (options: Object, vm: ?Component) {
  const props = options.props
  if (!props) return
  const res = {}
  let i, val, name
  if (Array.isArray(props)) {
    i = props.length
    while (i--) {
      val = props[i]
      if (typeof val === 'string') {
        name = camelize(val)
        res[name] = { type: null }
      } else if (process.env.NODE_ENV !== 'production') {
        warn('props must be strings when using array syntax.')
      }
    }
  } else if (isPlainObject(props)) {
    for (const key in props) {
      val = props[key]
      name = camelize(key)
      res[name] = isPlainObject(val)
        ? val
        : { type: val }
    }
  } else if (process.env.NODE_ENV !== 'production') {
    warn(
      `Invalid value for option "props": expected an Array or an Object, ` +
      `but got ${toRawType(props)}.`,
      vm
    )
  }
  options.props = res
}

我们看到normalizeProps函数接受两个参数:第一个是开发者传递的options配置,第二个是可选的当前组件实例,主要用于非生产环境检测异常(props格式非数组也非对象)后打印警告。

function normalizeProps (options: Object, vm: ?Component) {
  const props = options.props
  if (!props) return
  const res = {}
  let i, val, name
  //暂时忽略此处代码
  options.props = res
}

我们将代码缩减下看看,首先读取当前开发者配置中的props,若不存在则直接返回。之后声明了新对象res,用于保存规范后的结果输出,同时又声明了ivalname三个变量供后面使用。

function normalizeProps (options: Object, vm: ?Component) {
  //暂时忽略
  if (Array.isArray(props)) {
    i = props.length
    while (i--) {
      val = props[i]
      if (typeof val === 'string') {
        name = camelize(val)
        res[name] = { type: null }
      } else if (process.env.NODE_ENV !== 'production') {
        warn('props must be strings when using array syntax.')
      }
    }
  } else if (isPlainObject(props)) {
    for (const key in props) {
      val = props[key]
      name = camelize(key)
      res[name] = isPlainObject(val)
        ? val
        : { type: val }
    }
  } else if (process.env.NODE_ENV !== 'production') {
    warn(
      `Invalid value for option "props": expected an Array or an Object, ` +
      `but got ${toRawType(props)}.`,
      vm
    )
  }
  options.props = res
}

我们可以看到,这里执行了三个if分支判断。其中用到了camelize方法,可移步查看camelize

  • 若是数组格式,while循环遍历数组的每个元素。只有元素是字符串格式的时候,改成{type:null}格式。例如开发者写的是props:['yourData'],转换后的结果就是props:{yourData:{type:null}}。若元素非字符串且非生产环境下则打印警告。
  • 若是纯对象格式,for循环遍历对象的每个元素。先保存value结果,在规范命名规则,之后判断若val是纯对象,则直接使用,否则改成{type:val}的格式。比如
// 例子一:
props:{
  yourData:Number
}
// 将会被修改为以下格式
props:{
  yourData:{
    type:Number
  }
}


//例子二:
props:{
  yourData:{
    type:Number
  }
}
// 不做格式转变,直接使用。至于是否包含默认值,对转换结果没影响

总结:其实规范化props很简单,只是将数据修改为纯对象格式。对象增加一个type属性,若有指定类型则显示类型,否则为null。若有默认值default也会包含在里面。

normalizeInject

详细了解了如何规范化props之后,再看另外两个规范想必就非常容易了,这里就简单介绍了:

function normalizeInject (options: Object, vm: ?Component) {
  const inject = options.inject
  if (!inject) return
  const normalized = options.inject = {}
  if (Array.isArray(inject)) {
    for (let i = 0; i < inject.length; i++) {
      normalized[inject[i]] = { from: inject[i] }
    }
  } else if (isPlainObject(inject)) {
    for (const key in inject) {
      const val = inject[key]
      normalized[key] = isPlainObject(val)
        ? extend({ from: key }, val)
        : { from: val }
    }
  } else if (process.env.NODE_ENV !== 'production') {
    warn(
      `Invalid value for option "inject": expected an Array or an Object, ` +
      `but got ${toRawType(inject)}.`,
      vm
    )
  }
}

这里有一点非常有意思:先定义变量inject缓存options.inject的值,之后定义normalizedoptions.inject同时指向一个空对象。其中用到了extend方法,可移步查看extend

  • 若是数组格式,则遍历改成{yourKey:{from:yourKey}}格式;
  • 若是纯对象,若val是纯对象,则改成{yourKey:{from:yourKey,yourData:yourData}}格式;否则还是{yourKey:{from:yourKey}}格式。

normalizeDirectives

规范化directives

function normalizeDirectives (options: Object) {
  const dirs = options.directives
  if (dirs) {
    for (const key in dirs) {
      const def = dirs[key]
      if (typeof def === 'function') {
        dirs[key] = { bind: def, update: def }
      }
    }
  }
}

当且仅当directives存在且每一项的value值是函数的时候,数据修改前后如下:

// 修改前
{
  directives:{
    a:function(){}
  }
}
//修改后
{
  directives:{
    a:{
      bind:function(){},
      update:function(){}
    }
  }
}

总结:规范化数据格式至此告一段落,它们的存在只是为后面真正的合并做一个规范化处理,保持数据格式统一,开发者的命名规范。

mixins、extends的合并方式

我们知道mixins用于解决代码复用的问题。接下来的这段代码就是将开发者配置中可能存在的mixins混入合并到parent中。

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

首先判断!child._base的情况下才会执行上面代码。而child是开发者传递的配置,默认肯定是没有的,那肯定会执行里面的内容。那么什么情况下才会有child._base呢?我们知道_base只有Vue.config默认配置中有这么一个属性,且指向的是构造函数Vue本身,而合并是取两个对象的最大值为一个新对象,所以合并后的vm.$options._base结果肯定是有的了。所以结论是:只有第一次合并,是原始合并选项,不是另一次mergeOptions的结果再合并的时候,才会执行这里的代码。这么做是防止重复执行,一方面是节省性能,另一方面也是没必要。

  • 若开发者配置中存在mixins,则遍历mixins中的每个元素,递归调用mergeOptions方法合并产生一个新的对象,并赋值给parent
  • 而若开发者配置中存在extends,则更简单,因为extends只是一个对象,相当于mixins的一个元素,直接递归调用即可。

主要选项的合并方式

const options = {}
let key
for (key in parent) {
  mergeField(key)
}
for (key in child) {
  if (!hasOwn(parent, key)) {
    mergeField(key)
  }
}
function mergeField (key) {
  const strat = strats[key] || defaultStrat
  options[key] = strat(parent[key], child[key], vm, key)
}

做了这么多铺垫,现在才轮到真正合并的阶段。合并的原理并不复杂:

  • 首先创建一个用于最终输出结果的options对象;
  • for循环遍历parent合并到options中;
  • for循环遍历child,只有parent中没有的key对象才能合并到options中(防止开发者覆盖默认配置);
  • mergeField函数是核心合并方式。在这之前先定义了strats策略对象,对象上分别定义了elpropsDatadata生命周期componentsdirectivesfilterswatchpropsmethodsinjectcomputedprovide的合并方式,称为选项合并策略,即不同的模块采用不同的合并方式;
  • 若找不到指定合并方式,例如开发者定义了一个特殊的选项:child.aabbcc = {},这个时候strats.aabbcc的结果是undefined,这种情况则会调用默认合并方法defaultStrat
  • 默认合并方法是在没指定合并策略的前提下使用的,若开发者定义了一个特殊的选项child.aabbcc={},我们也可以提前在全局配置下定义同名合并方法:Vue.config.optionMergeStrategies.aabbcc = function(){}。这就是Vue文档上提到的自定义选项合并策略

接下来我们来一一查看各个模块的合并方法。

el、propsData 合并策略

if (process.env.NODE_ENV !== 'production') {
  strats.el = strats.propsData = function (parent, child, vm, key) {
    if (!vm) {
      warn(
        `option "${key}" can only be used during instance ` +
        'creation with the `new` keyword.'
      )
    }
    return defaultStrat(parent, child)
  }
}

在非生产环境定义了strats.elstrats.propsData,其最终调用的还是默认合并方法defaultStrat。这里有人就要奇怪了,为何只在非生产环境定义,生产环境怎么办?其实生产环境也是调用的默认合并方法defaultStrat,因为strats.elstrats.propsData的结果是undefined,没有的话就会采用默认合并策略。
其实Vue中无论哪个环境,其最终输出结果必定是一致的。如果实现的过程有区别,那一定是为了方便开发调试,这里唯一的区别是多了一个if(!vm){}判断没有vm实例的情况下打印警告,提示el选项或者propsData选项只能在使用new操作符创建实例的时候可用。这也说明了,如果拿不到vm则说明处理的是子组件选项。

defaultStrat 默认合并策略

const defaultStrat = function (parentVal: any, childVal: any): any {
  return childVal === undefined
    ? parentVal
    : childVal
}

接下来就是策略合并的默认方法defaultStrat。当一个选项不需要特殊处理的时候,就使用默认合并策略。逻辑很简单:若childVal存在则直接返回,否则返回parentVal

data 合并策略

strats.data = function (
  parentVal: any,
  childVal: any,
  vm?: Component
): ?Function {
  if (!vm) {
    if (childVal && typeof childVal !== 'function') {
      process.env.NODE_ENV !== 'production' && warn(
        'The "data" option should be a function ' +
        'that returns a per-instance value in component ' +
        'definitions.',
        vm
      )

      return parentVal
    }
    return mergeDataOrFn(parentVal, childVal)
  }

  return mergeDataOrFn(parentVal, childVal, vm)
}

data的合并策略代码如上所示,其最终主要使用mergeDataOrFn方法处理,有三种可能性:

  • 如果是子组件选项,且开发者所写的data不是函数的情况下(data必须是函数)则不做合并处理,直接返回parentVal结果;
  • 如果是子组件选项,且开发者所传data格式合规(是函数)的情况下,直接调用mergeDataOrFn方法处理data结果;
  • vm存在的情况下,也就是当new Vue的时候(因为这个时候vm值是必然存在的),也直接调用mergeDataOrFn方法处理data结果。

总结:子组件与根组件合并data选项都是调用了mergeDataOrFn方法处理,唯一的区别是是否传vm参数。

那么mergeDataOrFn方法到底是怎么处理的呢?我们先来看下源码:

export function mergeDataOrFn (
  parentVal: any,
  childVal: any,
  vm?: Component
): ?Function {
  if (!vm) {
    // in a Vue.extend merge, both should be functions
    if (!childVal) {
      return parentVal
    }
    if (!parentVal) {
      return childVal
    }
    // when parentVal & childVal are both present,
    // we need to return a function that returns the
    // merged result of both functions... no need to
    // check if parentVal is a function here because
    // it has to be a function to pass previous merges.
    return function mergedDataFn () {
      return mergeData(
        typeof childVal === 'function' ? childVal.call(this, this) : childVal,
        typeof parentVal === 'function' ? parentVal.call(this, this) : parentVal
      )
    }
  } else {
    return function mergedInstanceDataFn () {
      // instance merge
      const instanceData = typeof childVal === 'function'
        ? childVal.call(vm, vm)
        : childVal
      const defaultData = typeof parentVal === 'function'
        ? parentVal.call(vm, vm)
        : parentVal
      if (instanceData) {
        return mergeData(instanceData, defaultData)
      } else {
        return defaultData
      }
    }
  }
}

通过上面代码我们可以看出mergeDataOrFn分为两种情况:

情况一:不存在vm属性的时候,说明处理的是子组件选项。

  • 根据注释可以得知:当前是调用Vue.extend函数时进行合并处理的,要求此时处理的父子data都必须是函数类型。
  • 接下来是两个if判断。如果没有childVal,说明子组件选项中没有data选项,则直接返回父组件选项。同样的,若负组件中不存在data选项,则无需合并,直接返回子组件data选项。
  • parentValchildVal都存在的情况下才会真正执行data的合并策略,直接返回一个mergedDataFn方法,此时data的合并代码直接执行结束,返回的是一个函数mergeDataFn。所以:data原本是一个函数,合并后仍然是一个函数,而不是一个纯对象。
  • mergeDataFn方法内部返回的是函数mergeData执行后返回的结果。而mergeData的两个参数则是子父data方法执行后返回的纯对象格式。

情况二:存在vm属性的时候,说明处理的是非子组件选项,也就是处理new操作符创建实例的情况。

  • 此时也是返回的一个未执行的函数mergedInstanceDataFn
  • 执行childValparentVal方法分别得到对应的纯对象instanceDatadefaultData。如果子类data方法存在,则直接调用mergeData方法合并两个对象为一个纯对象作为mergedInstanceDataFn函数的返回值,若不存在则直接返回父类对象作为mergedInstanceDataFn函数的返回值。

总结:mergeDataOrFn方法无论是否有vm属性,最终返回的永远是一个未执行的函数。内部只是调用了mergeData方法,将父子data函数执行后得到的纯对象合并之后得到合并后的纯对象。


上面提到了mergeData方法,那么这个方法是如何工作的呢?我们继续看它的源码:

function mergeData (to: Object, from: ?Object): Object {
  if (!from) return to
  let key, toVal, fromVal

  const keys = hasSymbol
    ? Reflect.ownKeys(from)
    : Object.keys(from)

  for (let i = 0; i < keys.length; i++) {
    key = keys[i]
    // in case the object is already observed...
    if (key === '__ob__') continue
    toVal = to[key]
    fromVal = from[key]
    if (!hasOwn(to, key)) {
      set(to, key, fromVal)
    } else if (
      toVal !== fromVal &&
      isPlainObject(toVal) &&
      isPlainObject(fromVal)
    ) {
      mergeData(toVal, fromVal)
    }
  }
  return to
}

此方法接受两个参数tofrom,后者非必需,若不存在,则直接返回to。从mergeDataOrFn函数中mergeData执行时的传参顺序看,to相当于childVal函数返回的对象,from相当于parentVal函数返回的对象。mergeData方法的作用是遍历from对象数据合并到to上,最终返回to对象。知道整体逻辑后,我们再详细拆解:

  • from不存在,那就没有合并的必要了,直接返回to
  • 声明三个未赋值的变量keytoValfromVal
  • 获取from对象的key值组成的数组并赋值给keys。至于怎么获取,这里做了个判断,若宿主环境支持原生symbolReflect,则使用Reflect.ownKeys获取,否则使用Object.keys方法获取;
  • 遍历对象,如果发现key值是__ob__,则跳过继续执行下一个循环。__obj__是啥?是响应式观测数据,后面会详细讲到,这里只要知道__ob__属性不会被合并即可;
  • 如果from对象中的key值不存在to中,则调用set函数对to设置对应的值;
  • 如果from对象中的key值存在to中,且from[key]to[key]不全等,且两者都是纯对象的情况下,则递归调用mergeData深度合并。

mergeData函数中用到了set函数,根据引用路径得知这个函数的位置:core/observer/index.js。里面的逻辑较多,后面在讲到响应式的时候会详细解释,目前我们只提取当前用到的代码,方便大家理解:

export function set (target: Array<any> | Object, key: any, val: any): any {
  //暂时忽略
  const ob = (target: any).__ob__
  //暂时忽略
  if (!ob) {
    target[key] = val
    return val
  }
  //暂时忽略
}

生命周期选项合并策略

源码中strats.data...合并策略之后就是生命周期选项的合并策略,源码如下:

// Hooks props 最终都会被合并为数组格式
function mergeHook (
  parentVal: ?Array<Function>,
  childVal: ?Function | ?Array<Function>
): ?Array<Function> {
  const res = childVal
    ? parentVal
      ? parentVal.concat(childVal)
      : Array.isArray(childVal)
        ? childVal
        : [childVal]
    : parentVal
  return res
    ? dedupeHooks(res)
    : res
}

function dedupeHooks (hooks) {
  const res = []
  for (let i = 0; i < hooks.length; i++) {
    if (res.indexOf(hooks[i]) === -1) {
      res.push(hooks[i])
    }
  }
  return res
}

LIFECYCLE_HOOKS.forEach(hook => {
  strats[hook] = mergeHook
})

//以下代码来自  src/shared/constants.js
export const LIFECYCLE_HOOKS = [
  'beforeCreate',
  'created',
  'beforeMount',
  'mounted',
  'beforeUpdate',
  'updated',
  'beforeDestroy',
  'destroyed',
  'activated',
  'deactivated',
  'errorCaptured',
  'serverPrefetch'
]

从上面代码可以很容易看出:遍历LIFECYCLE_HOOKS数组,将生命周期钩子函数每一项挂到strats策略对象上,全部指向mergeHook函数。这说明合并生命周期选项的核心就是mergeHook函数,整个函数体由三组三目运算符组成。我们接下来拆解看看mergeHook函数都做了些啥:

  • mergeHook函数接受两个参数,第一个是父类生命周期钩子,第二个是子类生命周期钩子;
  • childValparentVal都存在的情况下,则res是他们合并后的数组结果。注意:parentVal在前;
  • childVal存在,parentVal不存在的情况下,这个时候若childVal是数组则赋值给res,否则将childVal作为唯一元素组成数组后赋值给res
  • childVal不存在,则直接将parentVal赋值给res
  • mergeHook函数最后返回一个数组,若restrue,即存在的情况下,则返回去重后的数组;

学习了生命周期合并原则后,我们发现了一个新的好玩的东西:生命周期不仅仅可以写成一个函数,还可以写成函数组成的数组格式。

// 平时我们会这么写
{
  //代码忽略
  created:function(){
    console.log('created')
  }
  //代码忽略
}

//其实也可以这么写
{
  //代码忽略
  created:[
    function(){
      console.log('created1')
    },
    function(){
      console.log('created2')
    }
  ]
  //代码忽略
}

总结:生命周期合并最终都会被合并成一个数组的格式,他们并不会被相互替换。会本着父辈在前,子类在后的原则。实际执行的时候,也是本着这个原则。

assets 选项合并策略

function mergeAssets (
  parentVal: ?Object,
  childVal: ?Object,
  vm?: Component,
  key: string
): Object {
  const res = Object.create(parentVal || null)
  if (childVal) {
    process.env.NODE_ENV !== 'production' && assertObjectType(key, childVal, vm)
    return extend(res, childVal)
  } else {
    return res
  }
}

ASSET_TYPES.forEach(function (type) {
  strats[type + 's'] = mergeAssets
})
//以下代码来自  src/shared/constants.js
export const ASSET_TYPES = [
  'component',
  'directive',
  'filter'
]

通过ASSET_TYPES的内容我们可以看到,Vuecomponentsdirectivesfilters被认为是资源。与生命周期合并原则类似,遍历ASSET_TYPES分别在strats上指定合并策略方法是mergeAssets。此方法的逻辑也很简单:

  • 创建一个原型为parentVal的对象res
  • 若子类不存在,则直接返回res
  • 若子类存在,则直接将childVal遍历复制到res中,最后直接返回res

其中,非生产环境调用了assertObjectType方法,源码如下:

function assertObjectType (name: string, value: any, vm: ?Component) {
  if (!isPlainObject(value)) {
    warn(
      `Invalid value for option "${name}": expected an Object, ` +
      `but got ${toRawType(value)}.`,
      vm
    )
  }
}

其目的就是在非生产环境下检测childVal,确保其为纯对象,若不是则给出警告。

总结:静态资源的合并就是先创建一个父辈为原型的空对象,将子类合并到空对象后返回

watch 合并策略

顺着源码位置继续往下,assets选项合并后就是watch选项合并,源代码主要如下:

strats.watch = function (
  parentVal: ?Object,
  childVal: ?Object,
  vm?: Component,
  key: string
): ?Object {
  // work around Firefox's Object.prototype.watch...
  if (parentVal === nativeWatch) parentVal = undefined
  if (childVal === nativeWatch) childVal = undefined
  /* istanbul ignore if */
  if (!childVal) return Object.create(parentVal || null)
  if (process.env.NODE_ENV !== 'production') {
    assertObjectType(key, childVal, vm)
  }
  if (!parentVal) return childVal
  const ret = {}
  extend(ret, parentVal)
  for (const key in childVal) {
    let parent = ret[key]
    const child = childVal[key]
    if (parent && !Array.isArray(parent)) {
      parent = [parent]
    }
    ret[key] = parent
      ? parent.concat(child)
      : Array.isArray(child) ? child : [child]
  }
  return ret
}
  • 首先确保父子都不是火狐浏览器对象原型链上的watch,若是,则置空。
  • 若子类不存在,则直接返回原型为父类的空对象;
  • 接下来是在非生产环境下检测子类是否是纯对象,若不是则给出警告;
  • 如果父类不存在,则直接返回子类;
  • 接下来就是父子都存在的情况下的合并。首先将父类遍历合并到空对象ret上。接下来就是遍历子类每一项,检测父类是否包含同名选项,若有则需确保父类同名选项为数组格式。
  • 最后将子类每一项复制ret对象中,若与父类名称冲突则返回与父类合并后的新数组,若不冲突则返回当前选项元素组成的新数组。 总结:合并后的watch选项,若父子存在同名,则同名元素的值为数组格式,否则还是一个函数

props、methods、inject、computed 合并策略

watch合并源代码后是这四个家伙的合并策略,期源代码逻辑如下:

strats.props =
strats.methods =
strats.inject =
strats.computed = function (
  parentVal: ?Object,
  childVal: ?Object,
  vm?: Component,
  key: string
): ?Object {
  if (childVal && process.env.NODE_ENV !== 'production') {
    assertObjectType(key, childVal, vm)
  }
  if (!parentVal) return childVal
  const ret = Object.create(null)
  extend(ret, parentVal)
  if (childVal) extend(ret, childVal)
  return ret
}

合并原理很简单:

  • 首先是非生产环境检测子类是否是纯对象,若不是则打印警告;
  • 若父类没有,则直接返回子类;
  • 创建一个没有原型的空对象,将父类对应的内容遍历拷贝到空对象ret中;
  • 若子类也存在,则将子类内容遍历拷贝到ret中;
  • 最后返回拷贝后的对象ret

总结:这四个选项的合并原则很简单,就是创建一个没有原型的纯对象,遍历拷贝父子类到新对象即可。

provide 合并策略

strats.provide = mergeDataOrFn

最后就是provide的合并策略,合并方法就是mergeDataOrFn。前面已经讲过,这里不再赘述。