Vue 配置合并

9/17/2021 Vue


# 参数合并的场景

# new Vue

vm.$options = mergeOptions(
  resolveConstructorOptions(vm.constructor),
  options || {},
  vm
)
1
2
3
4
5

# Vue.extend

这种情况下,mergeOptions 的参数有两个:

  • Super.optionsVue.options
  • extendOptions 即传递给 Vue.extend的参数





 


 














Vue.extend = function (extendOptions) {
  const Sub = function VueComponent(options) {
    this._init(options)
  }
  Sub.prototype = Object.create(Super.prototype)
  Sub.prototype.constructor = Sub
  Sub.options = mergeOptions(Super.options, extendOptions)
  Sub['super'] = Super
  // ...
  Sub.extend = Super.extend

  // ...

  // 缓存父类构造器的配置
  Sub.superOptions = Super.options
  // 缓存传入的选项对象
  Sub.extendOptions = extendOptions
  // 缓存自身的配置对象
  Sub.sealedOptions = extend({}, Sub.options)

  return Sub
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

暂时只考虑这两种简单的场景,因为 extend 方法的调用不一定非得是 Vue 构造器,还可以是通过 Vue.extend 方法生成的子类构造器

let Base = Vue.extend({
  // ...
})

let Child = Base.extend({
  // ...
})
1
2
3
4
5
6
7

这里还是以 创建 vue 实例,即 new Vue() 的这种情况为例

# mergeOptions

选项合并的重点是将用户自身传递的 options 选项Vue 构造函数自身的选项配置合并

vm.$options = mergeOptions(
  resolveConstructorOptions(vm.constructor),
  options || {},
  vm
)
1
2
3
4
5

该方法接收三个参数,首先第一个参数是通过 resolveConstructorOptions 这个方法获取的

# resolveConstructorOptions

该方法是用来从实例的构造器上去获取配置对象



 







 
 














function resolveConstructorOptions(Ctor) {
  var options = Ctor.options
  // extend 场景
  if (Ctor.super) {
    // 递归查询
    const superOptions = resolveConstructorOptions(Ctor.super)
    const cachedSuperOptions = Ctor.superOptions
    // 如果发生了变化,则需要更新到最新的配置
    if (superOptions !== cachedSuperOptions) {
      Ctor.superOptions = superOptions
      // 获取发生变化的配置项,这里会将 Ctor.options 与 Ctor.sealedOptions 进行对比
      const modifiedOptions = resolveModifiedOptions(Ctor)
      // update base extend options
      if (modifiedOptions) {
        // 只把发生修改的属性复制到extendOptions上
        extend(Ctor.extendOptions, modifiedOptions)
      }
      options = Ctor.options = mergeOptions(superOptions, Ctor.extendOptions)
      if (options.name) {
        options.components[options.name] = Ctor
      }
    }
  }
  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

上面将 vm.constructor 也就是 Vue 传入,在 resolveConstructorOptions 方法内部,

  • 判断如果 Ctor 上有 super 属性的话,也就是 Vue.extend 的场景下,会进一步对 options 进行处理
  • 没有的话就直接返回构造器上的 options 属性了。

当前 Vue 构造器上没有 super,所以就直接返回了 Vue.options

Vue.options这个属性又是什么时候定义的呢?

# Vue.options

其实在 实例化 Vue 之前,在源码中已经对 Vue 做了一系列的初始化操作,比如

# 给 Vue 的原型对象上混入属性和方法

function Vue(options) {
  this._init(options)
}

// 主要是在Vue的原型链上挂载一些方法
initMixin(Vue) // Vue.prototype._init
stateMixin(Vue) // Vue.prototype.$set/$delete/$watch
eventsMixin(Vue) // Vue.prototype.$on/$once/$off/$emit
lifecycleMixin(Vue) // Vue.prototype._update/$forceUpdate/$destroy
renderMixin(Vue) // Vue.prototype._render/$nextTick/renderHelpers
1
2
3
4
5
6
7
8
9
10

# 给 Vue 构造器初始化静态属性和静态方法

























 
 
 
 
 
 
 
 
 
 
 
 
 
 













export function initGlobalAPI(Vue: GlobalAPI) {
  // config
  const configDef = {}
  configDef.get = () => config
  Object.defineProperty(Vue, 'config', configDef)

  // exposed util methods.
  Vue.util = {
    warn,
    extend,
    mergeOptions,
    defineReactive,
  }

  Vue.set = set
  Vue.delete = del
  Vue.nextTick = nextTick

  // 2.6 explicit observable API
  Vue.observable = (obj: T): T => {
    observe(obj)
    return obj
  }

  Vue.options = Object.create(null)

  // Vue.options.components = {}
  // Vue.options.directives = {}
  // Vue.options.filters = {}
  ;['component', 'directive', 'filter'].forEach((type) => {
    Vue.options[type + 's'] = Object.create(null)
  })

  // 这个 _base 属性也很重要
  Vue.options._base = Vue

  // 内置组件 `KeepAlive`
  extend(Vue.options.components, builtInComponents)

  // Vue.use()
  initUse(Vue)
  // Vue.mixin()
  initMixin(Vue)
  // Vue.extend()
  initExtend(Vue)
  // Vue.component()
  // Vue.directive()
  // Vue.filter()
  initAssetRegisters(Vue)
}
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

可以看到 Vue.options 属性就是在 initGlobalAPI 这个方法中进行初始化的。

除此之外,针对不同的平台,还会在 options 属性上添加特定于平台的几个内部指令以及组件的定义,以 web 平台为例:

  • 指令包括:model, show
  • 组件包括:Transition, TransitionGroup
extend(Vue.options.directives, platformDirectives)
extend(Vue.options.components, platformComponents)
1
2

通过 debug,此时 Vue.options 大概长这个样子

  • _base 属性,指向的是 Vue 自身
  • components 属性中已经包含了三个内置组件:KeepAlive、Transition、TransitionGroup
  • directives 属性中也包含了两个内置指令:model、show

这样 mergeOptions 的第一个参数 Vue.options 就已经找到了,然后看下第二个参数:

以下面这个例子为例,

import App from './app'
new Vue({
  el: '#app',
  data() {
    return {
      name: 'Vue',
    }
  },
  render(h) {
    h(App)
  },
})
1
2
3
4
5
6
7
8
9
10
11
12

mergeOptions 的第二个参数 child 就是 new Vue() 时传入的选项对象

例子中只传入了 el、data、render 三个选项,所以不涉及 normalize 相关的规范化处理,然后就是 mergeField 的过程

export function mergeOptions(parent, child, vm?: Component): Object {
  // 组件校验
  checkComponents(child)

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

  // 规范化 props 配置
  normalizeProps(child, vm)
  // 规范化 inject 配置
  normalizeInject(child, vm)
  // 规范化 directives 配置
  normalizeDirectives(child)

  // 针对extends扩展的子类构造器
  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)
  }
  // 再遍历子对象上的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

选项合并过程中更多的不可控在于不知道用户传递了哪些配置选项,这些配置是否符合规范,是否达到合并配置的要求。因此每个选项的书写规则需要严格限定,原则上不允许用户脱离规则外来传递选项。因此在合并选项之前,很大的一部分工作是对选项的校验。

校验 & 规范化处理后,剩下的流程就是具体的合并操作了

  • 首先是遍历了 Vue.options 上的所有属性
  • 然后遍历了 vue 实例的 options 配置
  • 两种情况下都进行了 mergeField 操作

# 合并策略

Vue 允许用户自定义选项的合并策略 (opens new window)

另外如果某个属性 key 没有配置合并策略,就会走默认的策略。配置合并策略其实都是针对 选项对象 的,比如:props、data、lifecyle hooks、components、methods、watch 等等。

# 默认策略

默认的合并规则很简单,就是: 子对象上如果定义了就用子的,否则就用父亲的

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

# data 的合并

  • data 的合并策略,最终会返回一个函数 mergedInstanceDataFn
  • mergeData 的过程中,如果属性是响应式的,会被跳过
  • 如果不是响应式的对象,且 parent 上存在但 child 上不存在,则将它变成响应式对象之后再添加到 child 上
  • 如果父子对象上都存在且不相等,
strats.data = function (parentVal, childVal, vm?: Component): ?Function {
  // ...省略
  return mergeDataOrFn(parentVal, childVal, vm)
}

function mergeDataOrFn(parentVal, childVal, vm?: Component): ?Function {
  // ...省略

  // 这里返回的是一个函数,真正求值是在 initState 的时候
  return function mergedInstanceDataFn() {
    // 如果 data 是一个 function 就以 vm 为 this 调用
    const instanceData =
      typeof childVal === 'function' ? childVal.call(vm, vm) : childVal
    const defaultData =
      typeof parentVal === 'function' ? parentVal.call(vm, vm) : parentVal
    // child & parent 都存在就合并
    if (instanceData) {
      return mergeData(instanceData, defaultData)
    } else {
      // child 不存在,则使用 parent 的
      return defaultData
    }
  }
}

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]
    // 如果对象是响应式的就跳过
    if (key === '__ob__') continue

    toVal = to[key]
    fromVal = from[key]

    // 如果 child 上不存在,且不是响应式的对象,则将它变成响应式对象之后再添加到child上
    if (!hasOwn(to, key)) {
      Vue.set(to, key, fromVal)
    } else if (
      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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53

# provide 的合并

provide 的合并和 data 的合并差不多,跳过。

strats.provide = mergeDataOrFn
1

# lifecyle hooks 的合并

生命周期钩子函数的合并流程如下:

1.如果子和父都拥有钩子定义,则将子的和父的钩子合并, 合并的结果是将子的追加到父的后面

2.如果父不存在钩子,子存在时,则以数组形式返回子的钩子选项

3.当子不存在钩子选项时,则以父的返回

4.对结果进行去重处理

function mergeHook(
  parentVal: ?Array<Function>,
  childVal: ?(Function | ?Array<Function>)
): ?Array<Function> {
  // prettier-ignore
  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

举个例子

var Parent = Vue.extend({
  mounted() {
    console.log('parent')
  },
})
var Child = Parent.extend({
  mounted() {
    console.log('child')
  },
})
var vm = new Child().$mount('#app')

// 输出结果:
parent
child
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 资源类选项合并

包含components,directives以及filters,这几个选项必须是对象,他们的合并策略比较简单:

  • 首先创建了一个以父的资源选项为原型的对象
  • 然后将子的定义拷贝到这个对象,如果父上存在同名的属性,就会被覆盖掉
  • 最后返回这个对象
function mergeAssets(
  parentVal: ?Object,
  childVal: ?Object,
  vm?: Component,
  key: string
): Object {
  // 创建一个原型指向父的资源选项的对象
  const res = Object.create(parentVal || null)
  if (childVal) {
    // 将子选项的值赋值给这个对象
    return extend(res, childVal)
  } else {
    return res
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

举个例子

const KeepAlive = {
  name: 'KeepAlive',
  abstract: false,
}

let vue = new Vue({
  components: { KeepAlive },
})

console.log(vue.$options.components)
1
2
3
4
5
6
7
8
9
10

返回值形式如下

{
  KeepAlive: {
    name: "KeepAlive",
    abstract: false,
  },
  [[Prototype]]: {
    KeepAlive: {
      name: 'keep-alive',
      abstract: true,
      // ...
    },
    Transition: {
      // ...
    },
    TransitionGroup: {
      // ...
    },
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# watch 的合并

  • 子不存在,返回以父为原型的对象
  • 父不存在,返回子
  • 都存在时,则遍历子上的所有定义
    • 父上存在,则将子的定义追加到父的数组中返回
    • 父上不存在,则将子的定义以数组的形式返回
strats.watch = function (parentVal, childVal, vm, key) {
  // const nativeWatch = ({}).watch
  // work around Firefox's Object.prototype.watch...
  if (parentVal === nativeWatch) parentVal = undefined
  if (childVal === nativeWatch) childVal = undefined
  // 子不存在,直接返回以父为原型的对象
  if (!childVal) return Object.create(parentVal || null)
  if (process.env.NODE_ENV !== 'production') {
    assertObjectType(key, childVal, vm)
  }
  // 父不存在,则返回子
  if (!parentVal) return childVal

  const ret = {}

  // 从父上拷贝一份到 ret
  extend(ret, parentVal)

  // 遍历子
  for (const key in childVal) {
    // 从父上获取当前key的值
    let parent = ret[key]
    // 从子上获取当前key的值
    const child = childVal[key]

    if (parent && !Array.isArray(parent)) {
      parent = [parent]
    }

    // prettier-ignore
    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
30
31
32
33
34
35
36

举个例子

let Parent = Vue.extend({
  data() {
    return {
      msg: 'Parent',
    }
  },
  watch: {
    msg() {
      console.log('Parent change')
    },
  },
})

let Child = Vue.extend({
  data() {
    return {
      msg: 'Child',
    }
  },
  watch: {
    msg() {
      console.log('Child change')
    },
  },
})

let vm = new Child()
vm.msg = 'hello'
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

执行结果

parent change
child change
1
2

# 其他选项的合并

包括 propsmethodsinjectcomputed 选项的合并,规则也很简单

  • 父不存在则直接返回子
  • 都存在,则用子去覆盖父
// prettier-ignore
strats.props =
strats.methods =
strats.inject =
strats.computed =
  function (parentVal, childVal, vm, key): ?Object {
    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

# 合并结果

合并完成之后,vm.$options 大概长下面这个样子

vm.$options = {
  _base: function Vue() {
    // ...
  },
  components: {},
  data: function mergedInstanceDataFn() {
    // ...
  },
  directives: {},
  el: '#app',
  filters: {},
  created: [
    function created() {
      console.log('this is main.js')
    },
  ],
  render: function (h) {},
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Last Updated: 10/21/2024, 4:15:17 PM