vue源码分析(十六) 选项合并之合并策略

10/16/2018 vue源码分析面试

# 1. 概述

​ 上一章我们分析了 Vue 选项合并中的规范化,在这一章中我们重点来看看Vue 选项合并中的合并策略。

​ 在分析之前,我们还是以上一章的案例进行讲解,如下:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>vue 源码分析</title>
  <script src="../../dist/vue.js"></script>
</head>
<body>
  <div id="app"></div>
    <script>
      // Vue.config.devtools = true
      // Vue.config.performance = true
      new Vue({
        el: '#app',
        template: `
          <div> {{ name }} </div>
        `,
        data: {
          name: 'robin'
        }
      })
    </script>
</body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

​ 我们继续往下看,接下来是一段如下的代码:

​ 源码目录:src/core/util/options.js

export function mergeOptions (
  parent: Object,
  child: Object,
  vm?: Component
): Object {
  /*省略...*/
  // 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) {
    if (child.extends) {
      parent = mergeOptions(parent, child.extends, vm);
    }

    if (child.mixins) {
      for (var i = 0, l = child.mixins.length; i < l; i++) {
        parent = mergeOptions(parent, child.mixins[i], vm);
      }
    }
  }
  /*省略...*/
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

​ 在上一章 (opens new window)中我们分析过了参数 parentchild 的值在我们当前案例中,这两个参数分别为,如下:

// parent
parent = {
	components: {
		KeepAlive
		Transition,
    	TransitionGroup
	},
	directives:{
	    model,
        show
	},
	filters: Object.create(null),
	_base: Vue
}

// child
child = {
  el: "#app",
  template: "<div> {{ name }} </div>",
  data: {
    name: "robin"
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

​ 知道了parentchild 的参数格式,我们继续看看 mergeOptions 函数中的这段代码,这段代码的作用是处理 extends 选项和 mixins 选项。首先判断我们传进来的 optionschild 是否存在 _base 属性,如果不存在继续执行后面的语句, 然后判断 child.extends 是否存在,如果存在的话就递归调用 mergeOptions 函数将 parentchild.extends 进行合并,并将结果作为新的 parent

​ 接着检测 child.mixins 选项是否存在,如果存在则使用同样的方式进行操作,不同的是,由于 mixins 是一个数组所以要遍历一下。

说明:由于处理 extendsmixins是通过递归调用mergeOptions函数,所有我们在分析完 mergeOptions 以后在来看看这两个处理逻辑。

​ 我们继续往下看,代码如下:

​ 源码目录:src/core/util/options.js

const options = {}
let key
for (key in parent) {
  mergeField(key)
}
for (key in child) {
  if (!hasOwn(parent, key)) {
    mergeField(key)
  }
}
/*省略...*/
return options
1
2
3
4
5
6
7
8
9
10
11
12

​ 这段代码有两个 for 循环,分别是对 parentchild 的处理,我们分别来分析一下:

  • 第一个for...in 循环

​ 这个 for... in 用来遍历 parent,并且将 parent 对象的键作为参数传递给 mergeField 函数。前面我们已经知道 parent 对象包含 componentsdirectivesfilters 以及 _base 四个属性。

  • 第二个for...in 循环

​ 这个 for... in 用来遍历 child,并且将 child 对象的键作为参数传递给 mergeField 函数。前面我们已经知道 child 对象包含 eltemplatesdata 三个属性。

childfor... in 循环中唯一和 parent 区别是,如果 child 对象的键也在 parent 上出现,那么就不要再调用 mergeField 了,因为在上一个 for in 循环中已经调用过了,这就避免了重复调用。

​ 此处的 hasOwn 作用是用来判断一个属性是否是对象自身的属性(不包括原型上的)。

​ 接下来我们继续看看 mergeField 的定义,如下:

​ 源码目录:src/core/util/options.js

function mergeField (key) {
  const strat = strats[key] || defaultStrat
  options[key] = strat(parent[key], child[key], vm, key)
}
1
2
3
4

mergeField 逻辑很简单,首先通过传进来的 keystrats 对象上获取次 key 对应的值赋值给变量 strat,如果不存在则使用默认的 defaultStrat 赋值给变量 strat

​ 接下来通过调用 strat 引用的函数以此 keyparentchild 对应的值、vm实例、key 作为参数处理,将返回的结果赋值给此 keyoptions 对象上对应的属性。

# 2. 合并策略

​ 上一小节我们分析了 mergeOptions ,我们知道合并选项最终是通过各自的合并策略完成的,下面我们就分析一下所有的合并策略。

​ 我们通过调试工具,查看到的 strats 对象最终的结构,如下:

strats = {
  el: function (parent, child, vm, key) { /*defaultStrat...*/ },
  propsData: function (parent, child, vm, key) { /*defaultStrat...*/ },
  
  data: function ( parentVal, childVal, vm ) { /*mergeDataOrFn...*/ },
  
  beforeCreate: function mergeHook( parentVal, childVal ) { /*...*/ },
  created: function mergeHook( parentVal, childVal ) { /*...*/ },
  beforeMount: function mergeHook( parentVal, childVal ) { /*...*/ } ,
  mounted: function mergeHook( parentVal, childVal ) { /*...*/ },
  beforeUpdate: function mergeHook( parentVal, childVal ) { /*...*/ },
  updated: function mergeHook( parentVal, childVal ) { /*...*/ },
  beforeDestroy: function mergeHook( parentVal, childVal ) { /*...*/ },
  destroyed: function mergeHook( parentVal, childVal ) { /*...*/ },
  activated: function mergeHook( parentVal, childVal ) { /*...*/ },
  deactivated: function mergeHook( parentVal, childVal ) { /*...*/ },
  errorCaptured: function mergeHook( parentVal, childVal ) { /*...*/ },
  serverPrefetch: function mergeHook( parentVal, childVal ) { /*...*/ },
  
  components: function mergeAssets( parentVal, childVal, vm, key ) { /*...*/ },
  directives: function mergeAssets( parentVal, childVal, vm, key ) { /*...*/ },
  filters: function mergeAssets( parentVal, childVal, vm, key ) { /*...*/ },
  
  watch: function ( parentVal, childVal, vm, key ) { /*...*/ },
  
  props: function ( parentVal, childVal, vm, key ) { /*...*/ },
  methods: function ( parentVal, childVal, vm, key ) { /*...*/ },
  inject: function ( parentVal, childVal, vm, key ) { /*...*/ },
  computed: function ( parentVal, childVal, vm, key ) { /*...*/ },
  
  provide: function mergeDataOrFn( parentVal, childVal, vm ) { /*...*/ }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

# 2.1 选项el & propsData

​ 首先我们来看一下 el & propsData 的合并策略,如下:

​ 源码目录:src/core/util/options.js

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)
  }
}
1
2
3
4
5
6
7
8
9
10
11

​ 在非生产环境下,给 strats 对象上添加 elpropsData 属性,值为一个匿名函数。这个匿名函数是用来合并 el 选项和 propsData 选项的。

​ 我们再来看看匿名函数内部的逻辑,首先判断传递 vm 不存在的话直接报一个警告,如果存在则调用 defaultStrat 进行处理。此处需要注意这个警告的内容,表示在父组件中即使用new Vue 创建的实例中使用 elpropsData 选项,是没有什么问题,但是在子组件中也使用了 elpropsData 选项,这就会得到如上警告。

​ 如下声明,就会报出上面的警告:

// 子组件
const child = {
  el: '#child',
  template: `<div> {{ name }} </div>`,
  data() {
    return { name: 'robin' }
  }
}

// 父组件
new Vue({
  el: '#app',
  components: {child},
  template: `<child/>`,
  data: {
    name: 'robin'
  }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

​ 策略函数中的 vm 来自于 mergeOptions 函数的第三个参数。所以当调用 mergeOptions 函数且不传递第三个参数的时候,那么在策略函数中就拿不到 vm 参数。所以我们可以猜测到一件事,那就是 mergeOptions 函数除了在 _init 方法中被调用之外,还在 Vue.extend 方法中被调用的,此时调用 mergeOptions 函数就没有传递第三个参数,也就是说通过 Vue.extend 创建子类的时候 mergeOptions 会被调用,此时策略函数就拿不到第三个参数。

​ 在策略函数中通过判断是否存在 vm 就能够得知 mergeOptions 是在实例化时调用(使用 new 操作符走 _init 方法)还是在继承时调用(Vue.extend),而子组件的实现方式就是通过实例化子类完成的,子类又是通过 Vue.extend 创造出来的,所以我们就能通过对 vm 的判断而得知是否是子组件了。

​ 所以最终的结论就是:如果策略函数中拿不到 vm 参数,那么处理的就是子组件的选项

​ 接下来我们回到策略函数中,来分析 defaultStrat,定义如下:

​ 源码目录:src/core/util/options.js

const defaultStrat = function (parentVal: any, childVal: any): any {
  return childVal === undefined
    ? parentVal
    : childVal
}
1
2
3
4
5

​ 它的逻辑很简单,子选项不是 undefined 那么就是用子选项,否则使用父选项。

​ 但是大家还要注意一点,strats.elstrats.propsData 这两个策略函数是只有在非生产环境才有的,在生产环境下访问这两个函数将会得到 undefined,那这个时候 mergeField 函数的第一句代码就起作用了:

// 当一个选项没有对应的策略函数时,使用默认策略
const strat = strats[key] || defaultStrat
1
2

​ 所以在生产环境将直接使用默认的策略函数 defaultStrat 来处理 elpropsData 这两个选项。

# 2.2 选项 data

​ 我们再来看看选项 data的合并策略,如下:

​ 源码目录:src/core/util/options.js

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)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

​ 这段代码首先在 strats 对象上添加一个 data 属性,其值是一个函数。接下来我们看看,这个函数的内容,函数内部与 elpropsData 这两个策略函数相同,先判断是否传递了 vm 这个参数,我们知道当没有 vm 参数时,说明处理的是子组件的选项,那我们就看看对于子组件的选项它是如何处理的。

​ 首先判断是否传递了子组件的 data 选项(即:childVal),并且检测 childVal 的类型是不是 function,如果 childVal 的类型不是 function 则会给你一个警告,也就是说 childVal 应该是一个函数,如果不是函数会提示你 data 的类型必须是一个函数,这就是我们知道的:子组件中的 data 必须是一个返回对象的函数。如果不是函数,除了给你一段警告之外,会直接返回 parentVal

​ 如果 childVal 是函数类型,那说明满足了子组件的 data 选项需要是一个函数的要求,那么就直接返回 mergeDataOrFn 函数的执行结果。

​ 接下来如果 vm 存在说明处理的是使用 new 操作符创建实例时的选项,这个时候则直接返回 mergeDataOrFn 的函数执行结果。

​ 我们注意到了在选项 data 处理策略中,不管是子组件还是使用 new 操作符创建实例的选项,最终都是调用 mergeDataOrFn 函数,不同的是在处理使用 new 操作符创建实例的选项时,多传递了第三个参数 vm。那么问题来了为什么处理使用 new 操作符创建实例的选项要传递 vm

​ 接下我们带着这个问题再来分析 mergeDataOrFn 的源码,如下:

​ 源码目录:src/core/util/options.js

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
      }
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

​ 我们在前面的分析知道,当 vm 存在时说明处理的是非子组件,当 vm 不存时处理的是子组件,在处理子组件和非子组件时都调用了 mergeDataOrFn 函数,只是传递的参数不同,所以我们分析到这里知道前面我们提出的问题的答案了。mergeDataOrFn 中通过 vm 存在与否来判断处理的非子组件还是子组件。

vm 不存在说明处理的是子组件,执行 if 逻辑,vm 存在说明处理的是非子组件,执行 else 逻辑。

​ 我们继续往下看,在处理子组件的逻辑中,第一个 if 语句块,如果没有 childVal,也就是说子组件的选项中没有 data 选项,那么直接返回 parentVal,比如下面的代码:

Vue.extend({})
1

​ 我们使用 Vue.extend 函数创建子类的时候传递的子组件选项是一个空对象,即没有 data 选项,那么此时 parentVal 实际上就是 Vue.options,由于 Vue.options 上也没有 data 这个属性,所以压根就不会执行 strats.data 策略函数,也就更不会执行 mergeDataOrFn 函数,有的同学可能会问:既然都没有执行,那么这里的 return parentVal 是不是多余的?当然不多余,因为 parentVal 存在有值的情况。那么什么时候才会出现 childVal 不存在但是 parentVal 存在的情况呢?看下面的代码:

const Parent = Vue.extend({
  data: function () {
    return {
      test: 1
    }
  }
})

const Child = Parent.extend({})
1
2
3
4
5
6
7
8
9

​ 上面的代码中 Parent 类继承了 Vue,而 Child 又继承了 Parent,关键就在于我们使用 Parent.extend 创建 Child 子类的时候,对于 Child 类来讲,childVal 不存在,因为我们没有传递 data 选项,但是 parentVal 存在,即 Parent.options 下的 data 选项,那么 Parent.options 是哪里来的呢?实际就是 Vue.extend 函数内使用 mergeOptions 生成的,所以此时 parentVal 必定是个函数,因为 strats.data 策略函数在处理 data 选项后返回的始终是一个函数。

​ 由于 childValparentVal 必定会有其一,否则便不会执行 strats.data 策略函数,所以上面判断的意思就是:如果没有子选项则使用父选项,没有父选项就直接使用子选项,且这两个选项都能保证是函数,如果父子选项同时存在,则代码继续进行。

​ 接下当父子选项同时存在,那么就返回一个函数 mergedDataFn,注意:此时代码运行就结束了,因为函数已经返回了(return),至于 mergedDataFn 函数里面又返回了 mergeData 函数的执行结果这句代码目前还没有执行。

​ 以上就是 strats.data 策略函数在处理子组件的 data 选项时所做的事,我们可以发现 mergeDataOrFn 函数在处理子组件选项时返回的总是一个函数,这也就间接导致 strats.data 策略函数在处理子组件选项时返回的也总是一个函数。

​ 说完了处理子组件选项的情况,我们再看看处理非子组件选项的情况,也就是使用 new 操作符创建实例时的情况,执行的是 else 分支,它也返回的是一个函数 mergedInstanceDataFn ,注意此时的 mergedInstanceDataFn 函数同样还没有执行,它是 mergeDataOrFn 函数的返回值,所以这再次说明了一个问题:mergeDataOrFn 函数永远返回一个函数

​ 通过上面的分析我们可以知道 data 选项最终被 mergeOptions 函数处理成了一个函数,当合并处理的是子组件的选项时 data 函数可能是以下三者之一:

  • 1、就是 data 本身,因为子组件的 data 选项本身就是一个函数,即如下 mergeDataOrFn 函数的代码段所示:
export function mergeDataOrFn (
  parentVal: any,
  childVal: any,
  vm?: Component
): ?Function {
  if (!vm) {
    ...
    // 返回子组件的 data 选项本身
    if (!parentVal) {
      return childVal
    }
    ...
  } else {
    ...
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
  • 2、父类的 data 选项,如下代码段所示::
export function mergeDataOrFn (
  parentVal: any,
  childVal: any,
  vm?: Component
): ?Function {
  if (!vm) {
    ...
    // 返回父类的 data 选项
    if (!childVal) {
      return parentVal
    }
    ...
  } else {
    ...
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
  • 3、mergedDataFn 函数,如下代码段所示:
export function mergeDataOrFn (
  parentVal: any,
  childVal: any,
  vm?: Component
): ?Function {
  if (!vm) {
    ...
    // 返回 mergedDataFn 函数
    return function mergedDataFn () {
      return mergeData(
        typeof childVal === 'function' ? childVal.call(this, this) : childVal,
        typeof parentVal === 'function' ? parentVal.call(this, this) : parentVal
      )
    }
  } else {
    ...
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

​ 当合并处理的是非子组件的选项时 data 函数为 mergedInstanceDataFn 函数,如下代码段所示:

export function mergeDataOrFn (
  parentVal: any,
  childVal: any,
  vm?: Component
): ?Function {
  if (!vm) {
    ...
  } else {
    // 当合并处理的是非子组件的选项时 `data` 函数为 `mergedInstanceDataFn` 函数
    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
      }
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

​ 所以这就是我们一直强调的:data 选项最终被处理为一个函数。但是根据我们之前的分析可知,函数分几种情况,但它们都有一个共同的特点,即:这些函数的执行结果就是最终的数据

​ 我们可以发现 mergedDataFnmergedInstanceDataFn 这两个函数有一个共同的特点,内部都调用了 mergeData 处理数据并返回,我们先看一下 mergedDataFn 函数,其源码如下:

​ 源码目录:src/core/util/options.js

return function mergedDataFn () {
  return mergeData(
    typeof childVal === 'function' ? childVal.call(this, this) : childVal,
    typeof parentVal === 'function' ? parentVal.call(this, this) : parentVal
  )
}
1
2
3
4
5
6

​ 这个函数直接返回了 mergeData 函数的执行结果,再看看 mergedInstanceDataFn 函数,其源码如下:

​ 源码目录:src/core/util/options.js

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) { // childVal存在
    return mergeData(instanceData, defaultData)
  } else { // childVal不存在,返回parentVal
    return defaultData
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

​ 我们知道 childVal 要么是子组件的选项,要么是使用 new 操作符创建实例时的选项,无论是哪一种,总之 childVal 要么是函数,要么就是一个纯对象。所以如果是函数的话就通过执行该函数从而获取到一个纯对象,所以类似上面那段代码中判断 childValparentVal 的类型是否是函数的目的只有一个,获取数据对象(纯对象)。所以 mergedDataFnmergedInstanceDataFn 函数内部调用 mergeData 方法时传递的两个参数就是两个纯对象(当然你可以简单的理解为两个JSON对象)。

​ 那么接下来我们在看看mergeData 的源码,如下:

​ 源码目录:src/core/util/options.js

function mergeData (to: Object, from: ?Object): Object {
  // 没有 from 直接返回 to
  if (!from) return to
  let key, toVal, fromVal

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

  // 遍历 from 的 key
  for (let i = 0; i < keys.length; i++) {
    key = keys[i] // 获取对象的key
    // in case the object is already observed...
    if (key === '__ob__') continue
    toVal = to[key] // 获取key对应的目标对象的值
    fromVal = from[key] // 获取key对应的来源对象的值
    if (!hasOwn(to, key)) { // 如果 from 对象中的 key 不在 to 对象中,则使用 set 函数为 to 对象设置 key 及相应的值
      set(to, key, fromVal)
    } else if ( // 如果 from 对象中的 key 也在 to 对象中,且这两个属性的值都是纯对象则递归进行深度合并
      toVal !== fromVal &&
      isPlainObject(toVal) &&
      isPlainObject(fromVal)
    ) {
      // 深层递归
      mergeData(toVal, fromVal)
    }
    // 其他情况什么都不做
  }
  return to
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

​ 如果没有 from 则直接返回 to,也就是说如果没有 parentVal 产生的值,就直接使用 childVal 产生的值。

​ 如果有 parentVal 产生的值,则代码继续向下运行,我们看 mergeData 最后的返回值仍是 to 对象,所以你应该能猜的到 mergeData 函数的作用,可以简单理解为:from 对象的属性混合到 to 对象中,也可以说是将 parentVal 对象的属性混合到 childVal,最后返回的是处理后的 childVal 对象。

mergeData 的具体做法就是像上面 mergeData 函数的代码段中所注释的那样,对 from 对象的 key 进行遍历:

  • 如果 from 对象中的 key 不在 to 对象中,则使用 set 函数为 to 对象设置 key 及相应的值。
  • 如果 from 对象中的 keyto 对象中,且这两个属性的值都是纯对象则递归地调用 mergeData 函数进行深度合并。
  • 其他情况不做处理。

一、为什么最终 strats.data 会被处理成一个函数?

​ 这是因为,通过函数返回数据对象,保证了每个组件实例都有一个唯一的数据副本,避免了组件间数据互相影响。后面讲到 Vue 的初始化的时候大家会看到,在初始化数据状态的时候,就是通过执行 strats.data 函数来获取数据并对其进行处理的。

二、为什么不在合并阶段就把数据合并好,而是要等到初始化的时候再合并数据?

​ 这个问题是什么意思呢?我们知道在合并阶段 strats.data 将被处理成一个函数,但是这个函数并没有被执行,而是到了后面初始化的阶段才执行的,这个时候才会调用 mergeData 对数据进行合并处理,那这么做的目的是什么呢?

​ 其实这么做是有原因的,后面讲到 Vue 的初始化的时候,大家就会发现 injectprops 这两个选项的初始化是先于 data 选项的,这就保证了我们能够使用 props 初始化 data 中的数据,如下:

// 子组件:使用 props 初始化子组件的 childData 
const Child = {
  template: '<span></span>',
  data () {
    return {
      childData: this.parentData
    }
  },
  props: ['parentData'],
  created () {
    // 这里将输出 parent
    console.log(this.childData)
  }
}

var vm = new Vue({
    el: '#app',
    // 通过 props 向子组件传递数据
    template: '<child parent-data="parent" />',
    components: {
      Child
    }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

如上例所示,子组件的数据 childData 的初始值就是 parentData 这个 props。而之所以能够这样做的原因有两个

  • 1、由于 props 的初始化先于 data 选项的初始化
  • 2、data 选项是在初始化的时候才求值的,你也可以理解为在初始化的时候才使用 mergeData 进行数据合并。

三、你可以这么做。

​ 在上面的例子中,子组件的 data 选项我们是这么写的:

data () {
  return {
    childData: this.parentData
  }
}
1
2
3
4
5

​ 但你知道吗,你也可以这么写:

data (vm) {
  return {
    childData: vm.parentData
  }
}
// 或者使用更简单的解构赋值
data ({ parentData }) {
  return {
    childData: parentData
  }
}
1
2
3
4
5
6
7
8
9
10
11

​ 我们可以通过解构赋值的方式,也就是说 data 函数的参数就是当前实例对象。那么这个参数是在哪里传递进来的呢?其实有两个地方,其中一个地方我们前面见过了,如下面这段代码:

return function mergedDataFn () {
  return mergeData(
    typeof childVal === 'function' ? childVal.call(this, this) : childVal,
    typeof parentVal === 'function' ? parentVal.call(this, this) : parentVal
  )
}
1
2
3
4
5
6

​ 注意这里的 childVal.call(this, this)parentVal.call(this, this),关键在于 call(this, this),可以看到,第一个 this 指定了 data 函数的作用域,而第二个 this 就是传递给 data 函数的参数。

​ 当然了仅仅在这里这么做是不够的,比如 mergedDataFn 前面的代码:

if (!childVal) {
  return parentVal
}
if (!parentVal) {
  return childVal
}
1
2
3
4
5
6

​ 在这段代码中,直接将 parentValchildVal 返回了,我们知道这里的 parentValchildVal 就是 data 函数,由于被直接返回,所以并没有指定其运行的作用域,且也没有传递当前实例作为参数,所以我们必然还是在其他地方做这些事情,而这个地方就是我们说的第二个地方,它在哪里呢?当然是初始化的时候,后面我们会讲到的,如果这里大家没有理解也不用担心。

# 2.3 选项生命周期

​ 接下来我们再看一下选项生命周期合并策略,如下:

​ 源码目录:src/core/util/options.js

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

​ 这段代码是通过遍历 LIFECYCLE_HOOKS 数组,给 strats 策略对象上添加用来合并各个生命周期钩子选项的策略函数,并且这些生命周期钩子选项的策略函数相同:都是 mergeHook 函数

LIFECYCLE_HOOKS 是一个常量,定义如下:

​ 源码目录: src/shared/constants.js

export const LIFECYCLE_HOOKS = [
  'beforeCreate',
  'created',
  'beforeMount',
  'mounted',
  'beforeUpdate',
  'updated',
  'beforeDestroy',
  'destroyed',
  'activated',
  'deactivated',
  'errorCaptured'
]
1
2
3
4
5
6
7
8
9
10
11
12
13

​ 所以通过上面的遍历就会在 strats 策略对象上添加如上的 11 个属性,值都为mergeHook 函数。

​ 接下来我们再来看看mergeHook 函数,如下:

​ 源码目录:src/core/util/options.js

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
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

mergeHook 函数根据函数上方的注释可知最后被合并成了数组。函数体中 res 的结果由三组三元运算所得:

  • 如果子选项不存在直接等于父选项的值
  • 如果子选项存在,再判断父选项。如果父选项存在, 就把父子选项合并成一个数组
  • 如果父选项不存在,判断子选项是不是一个数组,如果不是数组将其作为数组的元素

​ 最后 res 有值则以它为参数调用 dedupeHooks函数,返回值作为最终结果返回,否则直接返回 res 。其中dedupeHooks 函数目的是为了剔除选项合并数组中的重复值。

​ 接下来我们在看看 dedupeHooks 的定义,如下:

​ 源码目录:src/core/util/options.js

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
}
1
2
3
4
5
6
7
8
9

dedupeHooks 函数中通过遍历钩子数组,去掉数组中的重复值。

注意: 生命周期钩子数组按顺序执行,因此先执行父选项中的钩子函数,后执行子选项中的钩子函数。

​ 下面我们举几个例子,来加强一下对 mergeHook 的理解,如下:

// 1
new Vue({
  created: function () {
    console.log('created')
  }
})
1
2
3
4
5
6

​ 如果以这段代码为例,那么对于 strats.created 策略函数来讲(注意这里的 strats.created 就是 mergeHooks),childVal 就是我们例子中的 created 选项,它是一个函数。parentVal 应该是 Vue.options.created,但 Vue.options.created 是不存在的,所以最终经过 strats.created 函数的处理将返回一个数组:

// res
options.created = [
  function () {
    console.log('created')
  }  
]
1
2
3
4
5
6

​ 再看下面的例子:

// 2
const Parent = Vue.extend({
  created: function () {
    console.log('parentVal')
  }
})

const Child = new Parent({
  created: function () {
    console.log('childVal')
  }
})
1
2
3
4
5
6
7
8
9
10
11
12

​ 其中 Child 是使用 new Parent 生成的,所以对于 Child 来讲,childVal 是:

created: function () {
  console.log('childVal')
}
1
2
3

​ 而 parentVal 已经不是 Vue.options.created 了,而是 Parent.options.created,那么 Parent.options.created 是什么呢?它其实是通过 Vue.extend 函数内部的 mergeOptions 处理过的,所以它应该是这样的:

Parent.options.created = [
  created: function () {
    console.log('parentVal')
  }
]
1
2
3
4
5

​ 所以这个例子最终的结果就是既有 childVal,又有 parentVal,那么根据 mergeHooks 函数的逻辑执行 parentVal.concat(childVal)这句,将 parentValchildVal 合并成一个数组。所以最终结果如下:

// res
[
  created: function () {
    console.log('parentVal')
  },
  created: function () {
    console.log('childVal')
  }
]
1
2
3
4
5
6
7
8
9

​ 另外我们注意第三个三目运算符:

: Array.isArray(childVal)
  ? childVal
  : [childVal]
1
2
3

​ 它判断了 childVal 是不是数组,这说明什么?说明了生命周期钩子是可以写成数组的,虽然 Vue 的文档里没有:

new Vue({
  created: [
    function () {
      console.log('first')
    },
    function () {
      console.log('second')
    },
    function () {
      console.log('third')
    }
  ]
})
1
2
3
4
5
6
7
8
9
10
11
12
13

# 2.4 选项资源

​ 接下来我们再看一下选项资源合并策略,如下:

​ 源码目录:src/core/util/options.js

ASSET_TYPES.forEach(function (type) {
  strats[type + 's'] = mergeAssets
})
1
2
3

​ 这段代码是通过遍历 ASSET_TYPES 数组,给 strats 策略对象上添加用来合并各个资源选项的策略函数,并且这些资源选项的策略函数相同:都是 mergeAssets 函数

ASSET_TYPES 是一个常量,定义如下:

​ 源码目录: src/shared/constants.js

export const ASSET_TYPES = [
  'component',
  'directive',
  'filter'
]
1
2
3
4
5

​ 所以通过上面的遍历就会在 strats 策略对象上添加如上的 3 个属性 componentsdirectivesfilters,值都为mergeAssets 函数。

​ 接下来我们再来看看mergeAssets 函数,如下:

​ 源码目录:src/core/util/options.js

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
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

​ 这段代码首先以 parentVal 为原型创建对象 res,然后判断是否有 childVal,如果有的话使用 extend 函数将 childVal 上的属性混合到 res 对象上并返回。如果没有 childVal 则直接返回 res

​ 接来我们举个例子,来加强一下对 mergeAssets 的理解,如下:

new Vue({
  el: '#app',
  components: {
    Child
  }
})
1
2
3
4
5
6

​ 上面的代码中,我们创建了一个 Vue 实例,并注册了一个子组件 Child,此时 mergeAssets 方法内的 childVal 就是例子中的 components 选项,而 parentVal 就是 Vue.options.components。我们在前面 vue源码分析(五) 静态属性和方法 (opens new window) 章节分析过了 Vue.options 的结构,如下:

Vue.options = {
	components: {
		KeepAlive
		Transition,
    TransitionGroup
	},
	directives:{
	    model,
      show
	},
	filters: Object.create(null),
	_base: Vue
}
1
2
3
4
5
6
7
8
9
10
11
12
13

​ 所以 mergeAssets 函数的参数列表中的 parentValchildVal 的值分别为,如下:

// parentVal
parentVal = {
  KeepAlive
  Transition,
  TransitionGroup
}

// childVal
childVal = {
  Child
}
1
2
3
4
5
6
7
8
9
10
11

​ 所以经过 Object.create(parentVal || null) 之后,在 res 的原型上添加了 KeepAliveTransitionTransitionGroup 三个属性,我们可以通过 . 语法或者 [key] 来获取对应的属性值。然后再经过 extend(res, childVal) 之后,res 变量将被添加 Child 属性,最终 res 如下:

res = {
  Child,
  // 原型
  __proto__: {
    KeepAlive,
    Transition,
    TransitionGroup
  }
}
1
2
3
4
5
6
7
8
9

​ 我们再回到 mergeAssets 函数中,看看这句话的作用,如下:

​ 源码目录:src/core/util/options.js

process.env.NODE_ENV !== 'production' && assertObjectType(key, childVal, vm)
1

​ 这句话的作用是在开发环境,判断childVal 是否是一个纯对象,如果不是则报一个警告,下面我们再来看看,assertObjectType 函数的定义,如下:

​ 源码目录:src/core/util/options.js

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
    )
  }
}
1
2
3
4
5
6
7
8
9

assertObjectType 很简单,就是使用 isPlainObject 进行判断。

# 2.5 选项 watch

​ 接下来我们再看一下选项 watch 合并策略,如下:

​ 源码目录:src/core/util/options.js

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
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

​ 这段代码是在strats 策略对象上添加 watch 属性,其值为一个函数,用来合并选项watch 的策略函数。这个函数内部首先两句代码是,如下:

​ 源码目录:src/core/util/options.js

// work around Firefox's Object.prototype.watch...
if (parentVal === nativeWatch) parentVal = undefined
if (childVal === nativeWatch) childVal = undefined
1
2
3

​ 在 Firefox 浏览器中 Object.prototype 拥有原生的 watch 函数,所以即便一个普通的对象你没有定义 watch 属性,但是依然可以通过原型链访问到原生的 watch 属性,这就会给 Vue 在处理选项的时候造成迷惑,因为 Vue 也提供了一个叫做 watch 的选项,即使你的组件选项中没有写 watch 选项,但是 Vue 通过原型访问到了原生的 watch。这不是我们想要的,所以上面两句代码的目的是一个变通方案,当发现组件选项是浏览器原生的 watch 时,那说明用户并没有提供 Vuewatch 选项,直接重置为 undefined

​ 其中在 nativeWatch,我们看一下它的定义,如下:

​ 源码目录:src/core/util/env.js

// Firefox has a "watch" function on Object.prototype...
export const nativeWatch = ({}).watch
1
2

nativeWatch 表示在 Firefox 中原生提供了 Object.prototype.watch 函数,所以当运行在 Firefox 中时 nativeWatch 为原生提供的函数,在其他浏览器中 nativeWatchundefined。这个变量主要用于 Vue 处理 watch 选项时与其冲突。

​ 接下来我们回到策略函数,继续往下看,如下:

​ 源码目录:src/core/util/env.js

if (!childVal) return Object.create(parentVal || null)
if (process.env.NODE_ENV !== 'production') {
  assertObjectType(key, childVal, vm)
}
if (!parentVal) return childVal
1
2
3
4
5

​ 首先检测了是否有 childVal,即组件选项是否有 watch 选项,如果没有的话,直接以 parentVal 为原型创建对象并返回(如果有 parentVal 的话)。

​ 如果组件选项有 watch 接下来在开发环境判断childVal 是否是一个纯对象,如果不是则报一个警告。如果是,继续判断是否有 parentVal,如果没有的话则直接返回 childVal,即直接使用组件选项的 watch。如果存在 parentVal,那么代码继续执行,此时 parentVal 以及 childVal 都将存在,那么就需要做合并处理了,如下:

​ 源码目录:src/core/util/env.js

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
1
2
3
4
5
6
7
8
9
10
11
12
13

​ 这段代码首先定一个变量 ret 用来合并 parentValchildVal,接下来使用 extend 函数将 parentVal 的属性混合到 ret 中,然后在通过 for...in 循环遍历 childVal

​ 在循环体中首先分别获取遍历到的 keyparentValchildVal 对象上的值分别为 parentchild,此时由于遍历的是 childVal 所以 parent 的值不一定存在有可能是 undefined 、而child 的值是一定存在的,所以接下来判断 parent 如果存在并且不是数组类型,则把其包装成数组类型赋值给 parent ,接下是两个三元运算,第一个判断parent 如果存在则把 parentchild 合并成一个数组赋值给 ret 上对应 key 属性的值,如果不存在继续第二个三元运算判断 child 是否为数组,如果是数组则把 child 直接赋值给 ret 上对应 key 属性的值,如果不是则把 child 包装成数组赋值给 ret 上对应 key 属性的值。

​ 和前面一样接来我们举个例子,来加强一下对选项 data 合并的理解,如下:

// 创建子类
const Sub = Vue.extend({
  // 检测 test 的变化
  watch: {
    test: function () {
      console.log('extend: test change')
    }
  }
})

// 使用子类创建实例
const v = new Sub({
  el: '#app',
  data: {
    test: 1
  },
  // 检测 test 的变化
  watch: {
    test: function () {
      console.log('instance: test change')
    }
  }
})

// 修改 test 的值
v.test = 2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

​ 上面的代码中,当我们修改 v.test 的值时,两个观察 test 变化的函数都将被执行。

​ 我们使用子类 Sub 创建了实例 v,对于实例 v 来讲,其 childVal 就是组件选项的 watch

watch: {
  test: function () {
    console.log('instance: test change')
  }
}
1
2
3
4
5

​ 而其 parentVal 就是 Sub.options,实际上就是:

watch: {
  test: function () {
    console.log('extend: test change')
  }
}
1
2
3
4
5

​ 最终这两个 watch 选项将被合并为一个数组:

watch: {
  test: [
    function () {
      console.log('extend: test change')
    },
    function () {
      console.log('instance: test change')
    }
  ]
}
1
2
3
4
5
6
7
8
9
10

​ 可以发现 watch.test 变成了数组,但是 watch.test 并不一定总是数组,只有父选项(parentVal)也存在对该字段的观测时它才是数组,如下:

// 创建实例
const v = new Vue({
  el: '#app',
  data: {
    test: 1
  },
  // 检测 test 的变化
  watch: {
    test: function () {
      console.log('instance: test change')
    }
  }
})

// 修改 test 的值
v.test = 2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

​ 我们直接使用 Vue 创建实例,这个时候对于实例 v 来说,父选项是 Vue.options,由于 Vue.options 并没有 watch 选项,所以逻辑将直接在 strats.watch 函数的这句话中返回:

if (!parentVal) return childVal
1

​ 没有 parentVal 即父选项中没有 watch 选项,则直接返回 childVal,也就是直接返回了子选项的 watch 选项,如就是例子中写的对象:

{
  test: function () {
    console.log('instance: test change')
  }
}
1
2
3
4
5

​ 所以此时 test 字段就不再是数组了,而就是一个函数。

​ 所以大家应该知道:被合并处理后的 watch 选项下的每个键值,有可能是一个数组,也有可能是一个函数

# 2.6 选项数据

​ 接下来我们再看一下选项数据合并策略,包括 propsmethodsinjectcomputed,如下:

​ 源码目录:src/core/util/options.js

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
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

​ 这段代码的作用是在 strats 策略对象上添加 propsmethodsinject 以及 computed 策略函数,这些策略函数是分别用来合并处理同名选项的,并且所使用的策略相同。

​ 对于 propsmethodsinject 以及 computed 这四个选项有一个共同点,就是它们的结构都是纯对象,虽然我们在书写 props 或者 inject 选项的时候可能是一个数组,但是在上一章规范化章节中我们知道,Vue 内部都将其规范化为了一个对象。

​ 首先,会检测 childVal 是否存在,即子选项是否有相关的属性,如果有的话在非生产环境下需要使用 assertObjectType 检测其类型,保证其类型是纯对象。然后会判断 parentVal 是否存在,不存在的话直接返回子选项。

​ 如果 parentVal 存在,则使用 extend 方法将其属性混合到新对象 ret 中,如果 childVal 也存在的话,那么同样会再使用 extend 函数将其属性混合到 ret 中,所以如果父子选项中有相同的键,那么子选项会把父选项覆盖掉。

# 2.7 选项provide

​ 接下来我们再看一下选项 provide 合并策略,如下:

​ 源码目录:src/core/util/options.js

strats.provide = mergeDataOrFn
1

provide 选项的合并策略与 data 选项的合并策略相同,都是使用 mergeDataOrFn 函数,这里就不重复讲解了。

# 3. 自定义合并策略

​ 关于 自定义选项合并策略 (opens new window) 可以查看官网的教程。

# 4. mixins

​ 我们再回到 mergeOptions 看看 mixins 的处理,代码如下:

​ 源码目录:src/core/util/options.js

if (child.mixins) {
  for (let i = 0, l = child.mixins.length; i < l; i++) {
    parent = mergeOptions(parent, child.mixins[i], vm)
  }
}
1
2
3
4
5

​ 在分析之前我们先来看一个例子,如下:

const mixin = {
  created () {
    console.log('this is mixins created')
  }
}

new Vue ({
  mixins: [mixin],
  created () {
    console.log('this is new Vue created ')
  }
})
1
2
3
4
5
6
7
8
9
10
11
12

​ 实例化执行会在控制台打印出如下的内容:

'this is mixins created'
'this is new Vue created'
1
2

mergeOptions 函数在处理 mixins 选项的时候递归调用了 mergeOptions 函数将 mixins 合并到了 parent 中,并将合并后生成的新对象作为新的 parent

​ 在我们这个例子中会循环合并 mixins 中的所有选项,当前只有一个 created 生命周期钩子函数,所以最终会调用生命周期的合并策略来处理即调用mergeHook,我们知道生命周期合并策略最终是将相同名称的钩子被合并成了数组,所以所有的生命周期钩子都会被执行。

​ 除了生命周期,任何写在 mixins 中的选项,都会使用 mergeOptions 中相应的合并策略进行处理,这就是 mixins 的实现方式。

# 5. extends

extends 选项,与 mixins 相同,只是由于 extends 选项只能是一个对象,而不能是数组,所以不需要遍历。

# 6. 总结

​ 现在我们了解了 Vue 中是如何合并处理选项的,接下来我们做一个总结:

  • 对于 elpropsData 选项使用默认的合并策略 defaultStrat
  • 对于 data 选项,使用 mergeDataOrFn 函数进行处理,最终结果是 data 选项将变成一个函数,且该函数的执行结果为真正的数据对象。
  • 对于 生命周期钩子 选项,将合并成数组,使得父子选项中的钩子函数都能够被执行
  • 对于 directivesfilters 以及 components 等资源选项,父子选项将以原型链的形式被处理,正是因为这样我们才能够在任何地方都使用内置组件、指令等。
  • 对于 watch 选项的合并处理,类似于生命周期钩子,如果父子选项都有相同的观测字段,将被合并为数组,这样观察者都将被执行。
  • 对于 propsmethodsinjectcomputed 选项,父选项始终可用,但是子选项会覆盖同名的父选项字段。
  • 对于 provide 选项,其合并策略使用与 data 选项相同的 mergeDataOrFn 函数。
  • 最后,以上没有提及到的选项都将使默认选项 defaultStrat
  • 最最后,默认合并策略函数 defaultStrat 的策略是:只要子选项不是 undefined 就使用子选项,否则使用父选项

​ 我们这个案例经过选项合并后的 $options 为,如下

vm.$options = {
  components: {},
  directives: {},
  filters: {},
  el: "#app",
  template: "<div> {{ name }} </div>",
  data: function mergedInstanceDataFn() {},
  _base: function Vue(options){}
}
1
2
3
4
5
6
7
8
9