vue源码分析(十五) 选项合并之规范化

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

# 1. 概述

​ 前面几章我们分析了 Vue 源码的整体结构、原型属性、静态属性、平台化以及全局配置,接下来的我们 Vue 实例化作为入口来分析,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

# 2. _init

​ 我们前面分析过了 new Vue 实质是执行了 Vue.prototype._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++
    /* 省略... */
  }
}
1
2
3
4
5
6
7
8

​ 首先声明变量 vm 赋值为 this ,我们知道此时的_init 方法是通过 vm._init(options) 调用的,所以此处的 thisVue 实例。

​ 接下来是在 Vue 实例上添加内部属性 _uid,作用是当前实例的唯一标示,每次实例化一个 Vue 实例,_uid 就会加一。

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

​ 源码目录:src/core/instance/init.js

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)
}
/* 省略... */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
  vm._name = formatComponentName(vm, false)
  mark(endTag)
  measure(`vue ${vm._name} init`, startTag, endTag)
}
1
2
3
4
5
6
7
8
9
10
11
12
13

​ 这段代码的主要作用是浏览器性能监控。

说明:

​ 关于 config.performance 可以参考官网 (opens new window)

​ 关于浏览器性能检测 performance API 可以查看 MDN (opens new window)这里 (opens new window)

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

​ 源码目录:src/core/instance/init.js

vm._isVue = true
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
  )
}
1
2
3
4
5
6
7
8
9
10
11
12
13

​ 首先给 Vue 实例添加一个 isVue 属性,用来标识一个对象是 Vue 实例,作用是可以避免该对象被响应系统观测。接下来判断配置项 options 存在并且 options._isComponent 属性为 true 才会执行 if 语句,否则执行 else 语句。

​ 这里 initInternalComponent 方法的作用是内部组件实例化,因为 Vue 动态合并策略非常慢,并且内部组件的选项都不需要特殊处理。

​ 我们当前案例会执行 else 语句,else语句中通过调用 mergeOptionsVue 实例化时传入的 optionsVue 自身的 options 进行合并保存到 vm.$options 。关于 $options 的作用可以查看官网说明 (opens new window)

说明:

​ 关于initInternalComponent 在我们当前案例中没有执行到,所以暂时不做分析,后面章节具体案例中我们在分析。

​ 关于 mergeOptions 我们会在下一小节分析。

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

​ 源码目录:src/core/instance/init.js

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) 
initState(vm)
initProvide(vm) 
callHook(vm, 'created') 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

​ 这段代码是一些初始化方法,主要是初始化生命周期初始化事件中心初始化渲染初始化 datapropscomputedwatcher初始化 provide/inject 等。

说明:

​ 关于初始化我们放在下一章节单独分析。

​ 我们再来看一下最后一度代码,如下:

​ 源码目录:src/core/instance/init.js

if (vm.$options.el) {
  vm.$mount(vm.$options.el)
}
1
2
3

​ 这段代码是 挂载 的逻辑,若 Vue 实例上面有 el 属性则直接执行 vm.$mount(vm.$options.el)挂载元素,若 Vue 实例上面没有 el 属性,则生命周期执行到这就挂起了,直到手动去执行 vm.mount(el),生命周期才会继续执行,接下来的重点就是 mount 这个方法究竟完成了哪些事情,这就是我们需要重点关注的。

# 3. 规范化

# 3.1 参数列表

接下来我们看看 mergeOptions 函数的调用,首先我们分析一下它的参数,如下:

  • resolveConstructorOptions(vm.constructor):当前实例构造函数的 options
  • optionsVue 实例化时传入的 options
  • vmVue 实例

​ 我们在 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

​ 而我们实例化 Vue 时传入的 options ,如下:

{
  el: "#app",
  template: "<div> {{ name }} </div>",
  data: {
    name: "robin"
  }
}
1
2
3
4
5
6
7

​ 所以 mergeOptions 的最终参数列表为,如下:

vm.$options = mergeOptions(
  // resolveConstructorOptions(vm.constructor)
  {
    components: {
      KeepAlive
      Transition,
      TransitionGroup
    },
    directives:{
      model,
      show
    },
    filters: Object.create(null),
    _base: Vue
  },
  // options || {}
  {
    el: "#app",
    template: "<div> {{ name }} </div>",
    data: {
      name: "robin"
    }
  },
  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

说明:

​ 关于 resolveConstructorOptions 我们放在组件化章节单独分析,在当前案例中我们只要知道返回的是 Vue.options 即可。

​ 分析完 mergeOptions 的参数我们再来分析 mergeOptions 函数的定义,如下:

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

/**
 * 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) // 对props进行一次规范化
  normalizeInject(child, vm) // 对inject进行一次规范化
  normalizeDirectives(child) // 对directives进行一次规范化

  // 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 (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)
    }
  }
  function mergeField (key) {
    const strat = strats[key] || defaultStrat
    options[key] = strat(parent[key], child[key], vm, key)
  }
  return options
}
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
42
43
44
45
46
47
48
49
50
51
52
53
54

# 3.2 检查组件

​ 首先我们看一下这个函数的注释,如下:

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

/**
 * Merge two option objects into a new one.
 * Core utility used in both instantiation and inheritance.
 */
1
2
3
4

​ 这句话的意思是将两个选项对象合并到一个新的对象中,用于实例化和继承的核心实用程序。这里要注意两点:

  • 第一,这个函数将会产生一个新的对象;

  • 第二,这个函数不仅仅在实例化对象(即_init方法中)的时候用到,在继承(Vue.extend)中也有用到,所以这个函数应该是一个用来合并两个选项对象为一个新对象的通用程序。

​ 接下来我们分析mergeOptions 的第一个 if语句 ,如下:

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

if (process.env.NODE_ENV !== 'production') {
  checkComponents(child)
}
1
2
3

​ 这段代码的作用是在开发环境,检查组件,参数是 child ,我们再来看 checkComponents 的定义,如下:

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

/**
 * Validate component names
 */
function checkComponents (options: Object) {
  for (const key in options.components) {
    validateComponentName(key)
  }
}
1
2
3
4
5
6
7
8

​ 通过注释我们知道这个函数的作用是校验组件名称,首先 checkComponents 通过 for in 循环遍历 options.components 属性中所有的组件,拿到组件的名称,最后通过 validateComponentName 函数校验组件名称。

​ 接下来我们找到 validateComponentName 的定义,如下:

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

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

​ 通过源码我们可以清楚的知道,组件的名称必须满足两个条件:

  1. 组件的名字由普通的字符和中横线(-)和一些特殊符号组成,且必须以字母开头。特殊符号包括 ·À-ÖØ-öø-ͽͿ-῿‌-‍‿-⁀⁰-↏Ⰰ-⿯、-퟿豈-﷏ﷰ-�
  2. 组件名字不能为内置的标签和保留标签

​ 我们再来看看 isBuiltInTag

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

export const isBuiltInTag = makeMap('slot,component', true)
1

​ 这段代码是判断标签是否为内置的标签有,目前 Vue 内置标签只有 slotcomponent

​ 关于 config.isReservedTag 我们再平台化的时候已经分析过了,可以查看 这里 (opens new window)

# 3.3 合并参数的类型

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

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

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

​ 这段代码是检查传入的 child 是否是函数,如果是的话,取到它的 options 选项重新赋值给 child 。所以说 child 参数可以是普通选项对象,也可以是 Vue 构造函数和通过Vue.extend 继承的子类构造函数。

# 3.4 规范化props

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

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

normalizeProps(child, vm)
1

​ 我们在 Vue官网 (opens new window)可以知道,props 的类型为 Array<string> | Object ,即 props 类型有数组和对象两种,其中数组中的元素都是字符串,我们举个例子,如下:

// 数组
const child = {
  props: ['age', 'msg']
}
// 对象
const child = {
  props: {
    age: Number,
    msg: {
      type: String,
      default: ''
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

​ 对应 props 值的类型有多种,但是在 Vue 内部处理的时候都统一规范化成一种类型,这样在选项合并的时候就能够统一处理,但是对于开发者来说,在使用的时候可以以多种写法使用。

​ 所以接下来我们看看 Vue 内部是如何对 props 进行规范化的,代码如下:

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

/**
 * Ensure all props option syntax are normalized into the
 * Object-based format.
 */
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
}
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

​ 首先通过 options.props 获取所有的 props ,如果 props 不存在则直接返回,如果存在继续执行规范化代码。接着定义一些变量如 resivalname ,其中:

  • res 用来存在规范化的最后结果
  • i 用来存储 props 的个数
  • val 用来存储每个 props 的值
  • name 用来存储每个 props 的名称

​ 我们继续往下看,接下来是一个 if...else if...else if... 语句,首先是 if 语句判断 props 为数组的形式,然后通过一个 while 循环处理每一个 propswhile 循环中首先获取到数组的每个元素,然后判断元素的类型,如果类型为 string 则通过 camelizeprops 中的每个元素中的横线转驼峰形式,然后在 res 对象上添加了与转驼峰后的 props 同名的属性,其值为 { type: null },这就是实现了对字符串数组的规范化,将其规范为对象的写法,只不过 type 的值为 null。如果不是 string 类型在开发环境中报一个警告。

else if 语句是对 props为对象的形式进行处理,首先通过 for...in... 循环获取到 props 中的所有元素的名称,然后通过对象获取值的方式即 props[key] 获取到每个名称对应的值,然后同样用camelizeprops 中的每个元素名称中的横线转驼峰形式,最后是一个三元运算符,判断 val 如果是一个纯对象则直接将 val 赋值给res 对象上转驼峰后的 props 同名的属性,如果不是则把 val 包装成一个对象的形式赋值给res 对象上转驼峰后的 props 同名的属性。

​ 最后如果 props 既不是数组也不是对象,则在开发环境报一个警告。

​ 所以通过 normalizeProps 规范化,我们最终得到的 props 为,如下:

// 数组规范化后的值
const child = {
  props: {
    age: {
      type: null
    },
    msg: {
      type: null
    }
  }
}
// 对象规范化后的值
const child = {
  age: {
    type: Number
  },
  props: {
    msg: {
      type: String,
      default: ''
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# 3.5 规范化inject

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

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

normalizeInject(child, vm)
1

​ 我们在 Vue官网 (opens new window)可以知道,inject 的类型为 Array<string> | { [key: string]: string | Symbol | Object } ,即 inject 类型有数组和对象两种,其中数组中的元素都是字符串,我们举个例子,如下:

// 数组
const child = {
  inject: ['age', 'msg']
}
// 对象
const child = {
  inject: {
    age: 'age1',
    msg: {
      from: 'bar',
      default: 'foo'
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

​ 和props 类似, inject 值的类型有多种,但是在 Vue 内部处理的时候都统一规范化成一种类型,这样在选项合并的时候就能够统一处理,但是对于开发者来说,在使用的时候可以以多种写法使用。

​ 所以接下来我们看看 Vue 内部是如何对 props 进行规范化的,代码如下:

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

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

​ 首先通过 options.inject 获取所有的 inject ,如果 inject 不存在则直接返回,如果存在继续执行规范化代码。接着定义一些变量如 normalized 其值和 options.inject 的值指向相同的引用,这样的好处是在接下来的的代码中修改了normalized 的值,options.inject 的值也会对应改变。

​ 我们继续往下看,接下来是一个 if...else if...else if... 语句,首先是 if 语句判断 inject 为数组的形式,然后通过一个 for 循环处理每一个 injectfor 循环中在 normalized 对象上添加了与 inject 同名的属性,其值为 { from: inject[i] }。这就是实现了对字符串数组的规范化,将其规范为对象的写法。

else if 语句是对 inject为对象的形式进行处理,首先通过 for...in... 循环获取到 inject 中的所有元素的名称,然后通过对象获取值的方式即 inject[key] 获取到每个名称对应的值,通过一个三元运算符,判断 val 如果是一个纯对象则直接将 val 赋值给normalized 对象上与 inject 同名的属性,如果不是则把 val 包装成一个对象的形式赋值给normalized 对象上与 inject 同名的属性。

​ 最后如果 inject 既不是数组也不是对象,则在开发环境报一个警告。

​ 所以通过 normalizeInject 规范化,我们最终得到的 inject 为,如下:

// 数组规范化后的值
const child = {
  props: {
    age: {
      form: 'age'
    },
    msg: {
      form: 'msg'
    }
  }
}
// 对象规范化后的值
const child = {
  age: {
    form: 'age1'
  },
  props: {
    msg: {
      from: 'bar',
      default: 'foo'
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# 3.6 规范化directives

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

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

normalizeDirectives(child)
1

​ 我们在 Vue官网 (opens new window)可以知道,directives 的类型为 Object ,即 directives 类型只有对象一种,我们举个例子,如下:

<div v-attrs="{ color: 'white', text: 'hello!' }">
  <input v-focus>
</div>

var vm = new Vue({
  el: '#app',
  data: {
    msg: 'this is vue directives demo'
  },
  // 注册两个局部指令
  directives: {
    focus: {
      inserted: function (el) {
        el.focus()
      }
    },
    attrs: function (el, binding) {
      console.log(binding.value.color) // => "white"
  		console.log(binding.value.text)  // => "hello!"
      el.style.backgroundColor = binding.value.color
    }
  }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

​ 虽然 directives 只有对象一种类型,但是我们可以从上面的是案例中发现,directives 的值由多种写法,例如上面案例中 focus 是一个对象,而 attrs 是一个函数,既然出现了允许多种写法的情况,那么当然要进行规范化了。

​ 所以接下来我们看看 Vue 内部是如何对 directives 进行规范化的,代码如下:

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

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

​ 首先通过 options.directives 获取所有的 directives ,如果 directives 存在继续执行规范化代码。接下来是一个 for...in... 循环获取到 directives 中的所有元素的名称,然后通过对象获取值的方式即 dirs[key] 获取到每个名称对应的值,接下来判断注册的指令是一个函数的时候,则将该函数作为对象形式的 bind 属性和 update 属性的值。也就是说,可以把使用函数语法注册指令的方式理解为一种简写。

​ 所以通过 normalizeDirectives 规范化,我们最终得到的 directives 为,如下:

var vm = new Vue({
  el: '#app',
  data: {
    msg: 'this is vue directives demo'
  },
  // 注册两个局部指令
  directives: {
    focus: {
      inserted: function (el) {
        el.focus()
      }
    },
    attrs: {
      bind: function (el, binding) {
        console.log(binding.value.color) // => "white"
        console.log(binding.value.text)  // => "hello!"
        el.style.backgroundColor = binding.value.color
      },
			update: function (el, binding) {
        console.log(binding.value.color) // => "white"
        console.log(binding.value.text)  // => "hello!"
        el.style.backgroundColor = binding.value.color
      }
    }
  }
})
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

​ 至此我们就彻底分析完了这三个用于规范化选项的函数的作用了, propsinject 以及 directives 这三个选项有了新的认识,同时也知道了 Vue 是如何做到允许开发者采用多种写法,也知道了 Vue 是如何统一处理的。接下来我们继续分析选项合并的其他逻辑。