• 合并配置
    • 外部调用场景
    • 组件场景
    • 总结

    合并配置

    通过之前章节的源码分析我们知道,new Vue 的过程通常有 2 种场景,一种是外部我们的代码主动调用 new Vue(options) 的方式实例化一个 Vue 对象;另一种是我们上一节分析的组件过程中内部通过 new Vue(options) 实例化子组件。

    无论哪种场景,都会执行实例的 _init(options) 方法,它首先会执行一个 merge options 的逻辑,相关的代码在 src/core/instance/init.js 中:

    1. Vue.prototype._init = function (options?: Object) {
    2. // merge options
    3. if (options && options._isComponent) {
    4. // optimize internal component instantiation
    5. // since dynamic options merging is pretty slow, and none of the
    6. // internal component options needs special treatment.
    7. initInternalComponent(vm, options)
    8. } else {
    9. vm.$options = mergeOptions(
    10. resolveConstructorOptions(vm.constructor),
    11. options || {},
    12. vm
    13. )
    14. }
    15. // ...
    16. }

    可以看到不同场景对于 options 的合并逻辑是不一样的,并且传入的 options 值也有非常大的不同,接下来我会分开介绍 2 种场景的 options 合并过程。

    为了更直观,我们可以举个简单的示例:

    1. import Vue from 'vue'
    2. let childComp = {
    3. template: '<div>{{msg}}</div>',
    4. created() {
    5. console.log('child created')
    6. },
    7. mounted() {
    8. console.log('child mounted')
    9. },
    10. data() {
    11. return {
    12. msg: 'Hello Vue'
    13. }
    14. }
    15. }
    16. Vue.mixin({
    17. created() {
    18. console.log('parent created')
    19. }
    20. })
    21. let app = new Vue({
    22. el: '#app',
    23. render: h => h(childComp)
    24. })

    外部调用场景

    当执行 new Vue 的时候,在执行 this._init(options) 的时候,就会执行如下逻辑去合并 options

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

    这里通过调用 mergeOptions 方法来合并,它实际上就是把 resolveConstructorOptions(vm.constructor) 的返回值和 options 做合并,resolveConstructorOptions 的实现先不考虑,在我们这个场景下,它还是简单返回 vm.constructor.options,相当于 Vue.options,那么这个值又是什么呢,其实在 initGlobalAPI(Vue) 的时候定义了这个值,代码在 src/core/global-api/index.js 中:

    1. export function initGlobalAPI (Vue: GlobalAPI) {
    2. // ...
    3. Vue.options = Object.create(null)
    4. ASSET_TYPES.forEach(type => {
    5. Vue.options[type + 's'] = Object.create(null)
    6. })
    7. // this is used to identify the "base" constructor to extend all plain-object
    8. // components with in Weex's multi-instance scenarios.
    9. Vue.options._base = Vue
    10. extend(Vue.options.components, builtInComponents)
    11. // ...
    12. }

    首先通过 Vue.options = Object.create(null) 创建一个空对象,然后遍历 ASSET_TYPESASSET_TYPES 的定义在 src/shared/constants.js 中:

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

    所以上面遍历 ASSET_TYPES 后的代码相当于:

    1. Vue.options.components = {}
    2. Vue.options.directives = {}
    3. Vue.options.filters = {}

    接着执行了 Vue.options._base = Vue,它的作用在我们上节实例化子组件的时候介绍了。

    最后通过 extend(Vue.options.components, builtInComponents) 把一些内置组件扩展到 Vue.options.components 上,Vue 的内置组件目前有 <keep-alive><transition><transition-group> 组件,这也就是为什么我们在其它组件中使用 <keep-alive> 组件不需要注册的原因,这块儿后续我们介绍 <keep-alive> 组件的时候会详细讲。

    那么回到 mergeOptions 这个函数,它的定义在 src/core/util/options.js 中:

    1. /**
    2. * Merge two option objects into a new one.
    3. * Core utility used in both instantiation and inheritance.
    4. */
    5. export function mergeOptions (
    6. parent: Object,
    7. child: Object,
    8. vm?: Component
    9. ): Object {
    10. if (process.env.NODE_ENV !== 'production') {
    11. checkComponents(child)
    12. }
    13. if (typeof child === 'function') {
    14. child = child.options
    15. }
    16. normalizeProps(child, vm)
    17. normalizeInject(child, vm)
    18. normalizeDirectives(child)
    19. const extendsFrom = child.extends
    20. if (extendsFrom) {
    21. parent = mergeOptions(parent, extendsFrom, vm)
    22. }
    23. if (child.mixins) {
    24. for (let i = 0, l = child.mixins.length; i < l; i++) {
    25. parent = mergeOptions(parent, child.mixins[i], vm)
    26. }
    27. }
    28. const options = {}
    29. let key
    30. for (key in parent) {
    31. mergeField(key)
    32. }
    33. for (key in child) {
    34. if (!hasOwn(parent, key)) {
    35. mergeField(key)
    36. }
    37. }
    38. function mergeField (key) {
    39. const strat = strats[key] || defaultStrat
    40. options[key] = strat(parent[key], child[key], vm, key)
    41. }
    42. return options
    43. }

    mergeOptions 主要功能就是把 parentchild 这两个对象根据一些合并策略,合并成一个新对象并返回。比较核心的几步,先递归把 extendsmixixns 合并到 parent 上,然后遍历 parent,调用 mergeField,然后再遍历 child,如果 key 不在 perent 的自身属性上,则调用 mergeField

    这里有意思的是 mergeField 函数,它对不同的 key 有着不同的合并策略。举例来说,对于生命周期函数,它的合并策略是这样的:

    1. function mergeHook (
    2. parentVal: ?Array<Function>,
    3. childVal: ?Function | ?Array<Function>
    4. ): ?Array<Function> {
    5. return childVal
    6. ? parentVal
    7. ? parentVal.concat(childVal)
    8. : Array.isArray(childVal)
    9. ? childVal
    10. : [childVal]
    11. : parentVal
    12. }
    13. LIFECYCLE_HOOKS.forEach(hook => {
    14. strats[hook] = mergeHook
    15. })

    这其中的 LIFECYCLE_HOOKS 的定义在 src/shared/constants.js 中:

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

    这里定义了 Vue.js 所有的钩子函数名称,所以对于钩子函数,他们的合并策略都是 mergeHook 函数。这个函数的实现也非常有意思,用了一个多层 3 元运算符,逻辑就是如果不存在 childVal ,就返回 parentVal;否则再判断是否存在 parentVal,如果存在就把 childVal 添加到 parentVal 后返回新数组;否则返回 childVal 的数组。所以回到 mergeOptions 函数,一旦 parentchild 都定义了相同的钩子函数,那么它们会把 2 个钩子函数合并成一个数组。

    关于其它属性的合并策略的定义都可以在 src/core/util/options.js 文件中看到,这里不一一介绍了,感兴趣的同学可以自己看。

    通过执行 mergeField 函数,把合并后的结果保存到 options 对象中,最终返回它。

    因此,在我们当前这个 case 下,执行完如下合并后:

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

    vm.$options 的值差不多是如下这样:

    1. vm.$options = {
    2. components: { },
    3. created: [
    4. function created() {
    5. console.log('parent created')
    6. }
    7. ],
    8. directives: { },
    9. filters: { },
    10. _base: function Vue(options) {
    11. // ...
    12. },
    13. el: "#app",
    14. render: function (h) {
    15. //...
    16. }
    17. }

    组件场景

    由于组件的构造函数是通过 Vue.extend 继承自 Vue 的,先回顾一下这个过程,代码定义在 src/core/global-api/extend.js 中。

    1. /**
    2. * Class inheritance
    3. */
    4. Vue.extend = function (extendOptions: Object): Function {
    5. // ...
    6. Sub.options = mergeOptions(
    7. Super.options,
    8. extendOptions
    9. )
    10. // ...
    11. // keep a reference to the super options at extension time.
    12. // later at instantiation we can check if Super's options have
    13. // been updated.
    14. Sub.superOptions = Super.options
    15. Sub.extendOptions = extendOptions
    16. Sub.sealedOptions = extend({}, Sub.options)
    17. // ...
    18. return Sub
    19. }

    我们只保留关键逻辑,这里的 extendOptions 对应的就是前面定义的组件对象,它会和 Vue.options 合并到 Sub.opitons 中。

    接下来我们再回忆一下子组件的初始化过程,代码定义在 src/core/vdom/create-component.js 中:

    1. export function createComponentInstanceForVnode (
    2. vnode: any, // we know it's MountedComponentVNode but flow doesn't
    3. parent: any, // activeInstance in lifecycle state
    4. ): Component {
    5. const options: InternalComponentOptions = {
    6. _isComponent: true,
    7. _parentVnode: vnode,
    8. parent
    9. }
    10. // ...
    11. return new vnode.componentOptions.Ctor(options)
    12. }

    这里的 vnode.componentOptions.Ctor 就是指向 Vue.extend 的返回值 Sub, 所以 执行 new vnode.componentOptions.Ctor(options) 接着执行 this._init(options),因为 options._isComponent 为 true,那么合并 options 的过程走到了 initInternalComponent(vm, options) 逻辑。先来看一下它的代码实现,在 src/core/instance/init.js 中:

    1. export function initInternalComponent (vm: Component, options: InternalComponentOptions) {
    2. const opts = vm.$options = Object.create(vm.constructor.options)
    3. // doing this because it's faster than dynamic enumeration.
    4. const parentVnode = options._parentVnode
    5. opts.parent = options.parent
    6. opts._parentVnode = parentVnode
    7. const vnodeComponentOptions = parentVnode.componentOptions
    8. opts.propsData = vnodeComponentOptions.propsData
    9. opts._parentListeners = vnodeComponentOptions.listeners
    10. opts._renderChildren = vnodeComponentOptions.children
    11. opts._componentTag = vnodeComponentOptions.tag
    12. if (options.render) {
    13. opts.render = options.render
    14. opts.staticRenderFns = options.staticRenderFns
    15. }
    16. }

    initInternalComponent 方法首先执行 const opts = vm.$options = Object.create(vm.constructor.options),这里的 vm.construction 就是子组件的构造函数 Sub,相当于 vm.$options = Object.create(Sub.options)

    接着又把实例化子组件传入的子组件父 VNode 实例 parentVnode、子组件的父 Vue 实例 parent 保存到 vm.$options 中,另外还保留了 parentVnode 配置中的如 propsData 等其它的属性。

    这么看来,initInternalComponent 只是做了简单一层对象赋值,并不涉及到递归、合并策略等复杂逻辑。

    因此,在我们当前这个 case 下,执行完如下合并后:

    1. initInternalComponent(vm, options)

    vm.$options 的值差不多是如下这样:

    1. vm.$options = {
    2. parent: Vue /*父Vue实例*/,
    3. propsData: undefined,
    4. _componentTag: undefined,
    5. _parentVnode: VNode /*父VNode实例*/,
    6. _renderChildren:undefined,
    7. __proto__: {
    8. components: { },
    9. directives: { },
    10. filters: { },
    11. _base: function Vue(options) {
    12. //...
    13. },
    14. _Ctor: {},
    15. created: [
    16. function created() {
    17. console.log('parent created')
    18. }, function created() {
    19. console.log('child created')
    20. }
    21. ],
    22. mounted: [
    23. function mounted() {
    24. console.log('child mounted')
    25. }
    26. ],
    27. data() {
    28. return {
    29. msg: 'Hello Vue'
    30. }
    31. },
    32. template: '<div>{{msg}}</div>'
    33. }
    34. }

    总结

    那么至此,Vue 初始化阶段对于 options 的合并过程就介绍完了,我们需要知道对于 options 的合并有 2 种方式,子组件初始化过程通过 initInternalComponent 方式要比外部初始化 Vue 通过 mergeOptions 的过程要快,合并完的结果保留在 vm.$options 中。

    纵观一些库、框架的设计几乎都是类似的,自身定义了一些默认配置,同时又可以在初始化阶段传入一些定义配置,然后去 merge 默认配置,来达到定制化不同需求的目的。只不过在 Vue 的场景下,会对 merge 的过程做一些精细化控制,虽然我们在开发自己的 JSSDK 的时候并没有 Vue 这么复杂,但这个设计思想是值得我们借鉴的。

    原文: https://ustbhuangyi.github.io/vue-analysis/components/merge-option.html