Vue源码学习(九) Vue中的options合并

配置合并

vue是一个学习成本很低的前端框架,开发者实际上只需要关注new Vue(options)中的配置即可,页面渲染,响应式数据的实现,vue 在背后做了大量的工作,本文主要分析vue实例以及组件实例的options配置是如何合并的。

vue中定义了自己的默认配置,然后将用户的配置和默认配置进行合并,这种代码设计方式在我们日常开发中野会用到,比如封装基础的ajax请求,默认请求方式是POST,但用户可以自定义请求方式为GET,只是vue在合并的实现上在代码的组织上更加精细。

Vue.options

Vue构造函数的静态属性options是在src/core/global-api/index.js中定义的.其中的ASSET_TYPES常量定义在src/shared/constants.js


export function initGlobalAPI (Vue: GlobalAPI) {
	...
	 Vue.options = Object.create(null)   //创建空对象
	  ASSET_TYPES.forEach(type => {     //遍历添加属性
	    Vue.options[type + 's'] = Object.create(null)
	  })
	initMixin(Vue)
	...

}

export const ASSET_TYPES = [
  'component',
  'directive',
  'filter'
]

从上面的代码可以看到在执行initGlobalAPI 方法后,Vue.options上会有components,directive,filters三个属性

Vue.options = {
	components: {},
	directives: {},
	filter: {}
}

initGlobalAPIsrc/core/index.js文件中执行。

Vue.mixin

initGlobalAPI中还执行了了initMixin方法,在src/core/global-api/mixin.js定义了该方法

import { mergeOptions } from '../util/index'

export function initMixin (Vue: GlobalAPI) {
  Vue.mixin = function (mixin: Object) {
    this.options = mergeOptions(this.options, mixin)
    return this
  }
}

可以看到initMixin方法给构造函数上添加了mixin方法,mixin 方法中调用mergeOptions方法对传入的mixinVue.options进行合并,然后返回Vue

看了mixin方法,我们可以看到该方法可以链式调用Vue.mixin(o1).mixin(o2),这种设计方式在库和框架中很常见,比如jQuery的很多方法都会返回jQuery构造函数,以能实现链式调用.

mergeOptions方法

mergeOptions 方法是用来对非组件的options进行合并以及在vue.mixin方法中进行调用,在src/core/util/options.js中定义了该方法.下面我们来分析下该方法

src/core/config.js中定义了合并策略

export default ({
	optionMergeStrategies: Object.create(null),
	...
})

可以看到optionMergeStrategies最初是一个空对象,在src/core/util/options.js中引入,并给该对象添加各种策略(方法)。

//src/core/util/options.js 伪代码
const strats = config.optionMergeStrategies
strats = {
	(el=propsData) : fn
	data:fn,
	[hook]: fn,
	watch: fn,
	(props = methods = inject = computed): fn,
	provide: mergeDataOrFn
}

这里使用了设计模式中的策略模式来设计配置项合并,关于策略模式请参考js设计模式之策略模式

下面我们来看看mergeOptions方法


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)
  normalizeInject(child, vm)
  normalizeDirectives(child)

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

可以看到对于options中不同的属性,合并策略是不同的。

钩子函数的合并

export const LIFECYCLE_HOOKS = [
  'beforeCreate',
  'created',
  'beforeMount',
  'mounted',
  'beforeUpdate',
  'updated',
  'beforeDestroy',
  'destroyed',
  'activated',
  'deactivated',
  'errorCaptured',
  'serverPrefetch'
]


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

/**
 * Hooks and props are merged as arrays.
 */
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
}

示例

示例代码

查看示例代码

Vue.mixin
// index.js
import Vue from 'vue'
import AppForMergeOptionsTest from '@components/AppForMergeOptionsTest.vue'

Vue
.mixin({
    created() {
        console.log('parent created')
    }
})
.mixin({
    testMixIn: 'testMixin'
})

new Vue({
    el: '#app',
    render(h) {
        return h(AppForMergeOptionsTest)
    }
})
// AppForMergeOptionsTest.vue
<template>
    <div>
        {{msg}}
    </div>
</template>

<script>

export default {
    name:"AppForMergeOptionsTest",
    created() {
        console.log('child created')
    },
    mounted() {
        console.log('child mounted')
    },
    data() {
        return {
            msg: 'hello world'
        }
    }
  
}
</script>

执行Vue.mixin方法,进入


Vue.mixin = function (mixin) {  /mixin{created: fn}
    this.options = mergeOptions(this.options, mixin);
    return this;
};

执行mergeOptions方法,针对本文中第一个mixin({created: fn}),最终在mergeHook里返回的rescreated:[f],第一个合并后的Vue.options如下图所示

在这里插入图片描述
在执行第二个mixin时,由于没有指定testMixIn具体的策略,会执行默认策略

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

可以看到执行完两次mixinVue.options是什么?
在这里插入图片描述

分组件和非组件实例进行配置合并

src/core/instance/init.js 中的_init方法中进行配置合并时分为两种类型


export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options?: Object) {
    
    const vm: Component = this
    // a uid
    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)
    }

    // 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 {
      vm.$options = mergeOptions( 
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }
    /* istanbul ignore else */
    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) // resolve injections before data/props
    initState(vm)
    initProvide(vm) // resolve provide after data/props
    callHook(vm, 'created')

    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
      vm._name = formatComponentName(vm, false)
      mark(endTag)
      measure(`vue ${vm._name} init`, startTag, endTag)
    }

    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }
}
  • 组件实例调用initInternalComponent方法进行合并配置
  • 非组件实例调用mergeOptions方法进行配置合并
initInternalComponent

export function initInternalComponent (
vm: Component, options: InternalComponentOptions) {
	...
  const opts = vm.$options = Object.create(vm.constructor.options)
	...
}

组件实例的vm.$options原型为vm.constructor.options,这里的vm.constructor指向子组件实例的构造函数Super

我们回顾下Super构造函数创建方法


Vue.extend = function (extendOptions: Object): Function {
    extendOptions = extendOptions || {}
    const Super = this
    const SuperId = Super.cid
    const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
    if (cachedCtors[SuperId]) {
      return cachedCtors[SuperId]
    }

    const name = extendOptions.name || Super.options.name
    if (process.env.NODE_ENV !== 'production' && name) {
      validateComponentName(name)
    }

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

    // For props and computed properties, we define the proxy getters on
    // the Vue instances at extension time, on the extended prototype. This
    // avoids Object.defineProperty calls for each instance created.
    if (Sub.options.props) {
      initProps(Sub)
    }
    if (Sub.options.computed) {
      initComputed(Sub)
    }

    // allow further extension/mixin/plugin usage
    Sub.extend = Super.extend
    Sub.mixin = Super.mixin
    Sub.use = Super.use

    // create asset registers, so extended classes
    // can have their private assets too.
    ASSET_TYPES.forEach(function (type) {
      Sub[type] = Super[type]
    })
    // enable recursive self-lookup
    if (name) {
      Sub.options.components[name] = Sub
    }

    // keep a reference to the super options at extension time.
    // later at instantiation we can check if Super's options have
    // been updated.
    Sub.superOptions = Super.options
    Sub.extendOptions = extendOptions
    Sub.sealedOptions = extend({}, Sub.options)

    // cache constructor
    cachedCtors[SuperId] = Sub
    return Sub
  }
}


这里的extendOptions就是组件对象

Sub.options = mergeOptions(
      Super.options,
      extendOptions
    )

Sub.options是将Vue.options和组件option进行合并,因此从const opts = vm.$options = Object.create(vm.constructor.options)这行代码可以看到,组件实例vm.$options中并没有组件option中定义的属性,而是在vm.$options.__proto__上。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值