mixin在vue框架中的定位是实现逻辑复用的功能,能够类比javascript中的混合继承方式。实现逻辑复用的方式有不少种,好比react提倡的高阶组件、hooks
等等,固然,在Vue中官方推荐的且使用频次最高的仍是mixin。javascript
本篇文章将会探讨Vue底层如何实现mixin,且mixin对vue各个配置项是如何处理的,以及混合的顺序如何等等问题。vue
组件调用mixin的方式有两种:java
不管以上使用了哪一种方式,最终调用的都是mergeOptions
这个工具方法。react
以Vue.mixin举例:api
// 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
}
}
复制代码
能够看到mergeOptions同字面意义同样,将多个options进行合并,生成一个新的options。数组
mergeOptions是vue中比较重要的辅助函数之一,除了在mixin中使用外还在extend、实例化阶段使用到:bash
// src/core/global-api/extend.js
Vue.extend = function (extendOptions: Object): Function {
...
Sub.options = mergeOptions(
Super.options,
extendOptions
)
}
// src/core/instance/init.js
Vue.prototype._init = function (options?: Object) {
...
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
复制代码
首先来看一下mergeOptions的主体代码:框架
// src/core/util/options.js
export function mergeOptions ( parent: Object, child: Object, vm?: Component ): Object {
...
// 规范化props
normalizeProps(child, vm)
// 规范化inject
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.
// 未合并的options不带有_base
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
}
复制代码
从代码逻辑上看,mergeOptions主要经历了两个步骤:ide
normalize同字面意思同样,用来规范化属性,好比props,可使用对象语法,可使用数组语法,而数组又能够是函数数组或者是字符串数组。因此normalize的做用就是统一将这些不一样的类型处理成对象类型的格式。函数
function normalizeProps (options: Object, vm: ?Component) {
const props = options.props
if (!props) return
const res = {}
let i, val, name
// props是数组类型
// props: [ 'someObjA', 'someObjB' ]
if (Array.isArray(props)) {
i = props.length
while (i--) {
val = props[i]
if (typeof val === 'string') {
// 将-改驼峰命名
name = camelize(val)
// string类型规范化为 { someObjA: { type: null } }
res[name] = { type: null }
} else if (process.env.NODE_ENV !== 'production') {
warn('props must be strings when using array syntax.')
}
}
} else if (isPlainObject(props)) {
// 对象类型的props
// props: { someObjA: String }
// props: { someObjA: [ Number, String ] }
// props: { someObjA: { type: Number, default: 1 } }
for (const key in props) {
// 若是是纯对象形式,如props类型3则直接使用,不然将属性后面的值做为type(如String)
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
}
复制代码
在normalizeProps中,字符串数组类型的props都会处理成type为null的类型,这里要注意的是,props会根据定义的type不一样,而给传进来的props给予不一样的默认值,好比咱们直接在组件模版上写require这个属性:
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++) {
// 字符串数组处理成 { bar: { from: 'bar' }}格式
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
)
}
}
复制代码
inject/provide是Vue 2.2.0版本引入特性,normalizeInject对inject的处理同props过程类似,都是处理成对象类型格式,但不一样的是,面对对象类型时normalizeInject又作了一层处理:
inject: {
foo: { someProperty: 'bar' }
}
// 处理后
inject: {
'foo': { from: 'foo', someProperty: 'bar' }
}
复制代码
这里对象的处理依旧是再次规范化了一下。
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 }
}
}
}
}
复制代码
normalizeDirectives对指令进行了规范化处理,一样统一处理成了对象类型。咱们知道Vue针对指令,提供了两种定义但方式:
// 处理前
directives: {
b: function () {
console.log('v-b')
}
}
// 处理后
directives: {
b: {
bind: function(){
console.log('v-b')
},
update: function(){
console.log('v-b')
}
}
}
复制代码
综上,props、inject、directive统一处理成了扩展度较高的对象类型格式,而且格式化后的数据会被从新赋值给传入的第一个参数(这里是child),以后就是递归处理被合并项的extends和mixins成员了,再递归合并以前先作了一次_base
的判断,这里的_base
指向Vue构造函数,_base
属性存在于Vue.options
上,因为组件初始化阶段必定会merge Vue options并返回一个新的options,因此被合并的options必定会存在_base
属性。
以上是进行合并前的数据处理阶段,而mixin真正重要的阶段实际上是mergeField
阶段,咱们知道两个组件options能够存在相同的选项,好比都具备methods
对象,但methods
对象挂载的方法可能相同可能不一样,其它选项也能够类比。mergeField
的做用就是考虑使用何种策略去处理这些选项,返回咱们须要的配置。
在mergeOptions函数逻辑最后,首先申请了一个新的存储对象options,将parant与child都通过mergeField处理再合并进options中。
相关代码:
function mergeField (key) {
// 策略模式-根据key选择不一样的处理函数
const strat = strats[key] || defaultStrat
// 调用处理函数,合并两个选项
options[key] = strat(parent[key], child[key], vm, key)
}
复制代码
当mixin两个普通的对象的时候,可使用深度优先去一层一层拷贝比对来合并值,但在vue中,简单的拷贝赋值并不能知足组件构造函数的需求,还须要将传进来的配置项进行处理。
所以组件实例化的过程,能够看做一个工厂,data、props、methods这些能够看做原材料,通过各个流水线工人的处理加工,就拿到了生产组件所须要的成品,这里的strat能够看到是加工原材料的工人。
因此理解mergeOptions的核心其实就是理解strate的过程。
首先来看Vue是如何定义strates的:
/** * Option overwriting strategies are functions that handle * how to merge a parent option value and a child option * value into the final value. * 选项覆盖策略是用来合并父选项值与子选项值到最终值的函数 */
const strats = config.optionMergeStrategies
复制代码
const defaultStrat = function(parentVal: any, childVal: any): any {
return childVal === undefined
? parentVal
: childVal
}
复制代码
defaultStrat表示默认的合并策略,childVal是须要处理的选项,当选项值不为undefined时,直接返回该选项。这也就意味着,那些未命中合并策略的选项将会被child中的选项直接覆盖。好比parent与child的组件options中均存在 demo: { ... } 属性,mixin后,parent中的demo不管定义为什么值都会被child中的demo覆盖。
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)
}
复制代码
对el和propsData处理是直接返回了默认策略,但返回以前加了对vm的判断,经过看warn能够得知el和propsData是经过new关键字实例化组件才可使用的属性。只要未传入vm变量,就不能声明这两个字段。
传入了vm的场景:
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
复制代码
未传入vm的场景:
也就是说,子组件和mixin对象不能定义el和propsData这两个字段。
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)
}
// provide
strats.provide = mergeDataOrFn
复制代码
合并data的逻辑异常繁琐,这也是很是必要的,由于data做为本地组件的状态管理器,挂载各类类型的状态,同时须要合并的data类型可能为对象也多是函数,但返回结果,必须为函数类型。
data字段的合并策略依旧是首先判断了子组件data的类型必须为函数。以后继续调用了mergeDataOrFn。
export function mergeDataOrFn(
parentVal: any,
childVal: any,
vm?: Component
): ?Function {
if (!vm) {
...
// extend或者mixin调用
return function mergedDataFn() {
return mergeData(
typeof childVal === 'function' ? childVal.call(this, this) : childVal,
typeof parentVal === 'function' ? parentVal.call(this, this) : parentVal
)
}
} else {
// new调用
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
}
}
}
}
复制代码
mergeDataOrFn函数最终返回了一个函数mergedDataFn或者mergedInstanceDataFn,函数值函数赋值给options.data属性,也就是说data属性最终会被处理成一个函数(防止引用传递),data属性真正合并的阶段放到了组件的初始化阶段。但不管是哪一种函数,mergeData拿到都是须要合并的两个对象。
function mergeData(to: Object, from: ?Object): Object {
if (!from) return to
let key, toVal, fromVal
// Object.keys拿不到不可枚举的属性
const keys = hasSymbol
? Reflect.ownKeys(from)
: Object.keys(from)
for (let i = 0; i < keys.length; i++) {
key = keys[i]
// in case the object is already observed...
// __ob__不合并
if (key === '__ob__') continue
toVal = to[key]
fromVal = from[key]
if (!hasOwn(to, key)) {
// 若to不存在该属性,则使用set赋值
set(to, key, fromVal)
} else if (
// 对象类型则深度合并
toVal !== fromVal &&
isPlainObject(toVal) &&
isPlainObject(fromVal)
) {
mergeData(toVal, fromVal)
}
}
return to
}
复制代码
从上面的过程能够看到真正执行data合并策略的过程在mergeData内部,而该函数倒是在组件初始化阶段才调用,这样作的主要目的实际上是为了保证data中能够访问到props对应的属性。
provide的合并策略与data相同。
function mergeHook( parentVal: ?Array<Function>, childVal: ?Function | ?Array<Function> ): ?Array<Function> {
// res返回了一个数组
// parentVal在与vue.options合并阶段不存在,因此不会命中parentVal.concat方法,则返回[childVal]
// 与以后的options合并的时候,parentVal必定是数组
const res = childVal
? parentVal
? parentVal.concat(childVal)
// 生命周期钩子函数为数组形式,直接返回该数组,
// 按数组顺序执行
: Array.isArray(childVal)
? childVal
: [childVal]
: parentVal
return res
? dedupeHooks(res)
: res
}
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
}
// 'beforeCreate', 'created', 'beforeMount', 'mounted', 'beforeUpdate', 'updated', 'beforeDestroy', 'destroyed', 'activated', 'deactivated', 'errorCaptured', 'serverPrefetch'
LIFECYCLE_HOOKS.forEach(hook => {
// { created(){}} -> {created: [function created(){}]}
strats[hook] = mergeHook
})
复制代码
mergeHook函数会将parent与child的生命周期钩子函数合并成数组形式,好比:
parent.options.created = function() {
console.log('parentCreated')
}
child.options.created = function() {
console.log('childCreated')
}
// 合并后
[
{
created: function() {
console.log('parentCreated')
}
},
{
created: function() {
console.log('childCreated')
}
}
]
复制代码
最后,生命周期的钩子函数会在callhook
中依次调用。虽然是依次执行的,但关于函数放置的顺序有一些须要注意的事项。
回到开头提到的mixin使用方式:
咱们编写一个demo来测试一下这两种方式的生命周期钩子函数调用的顺序:
// a.js
export default {
created() {
console.log('a')
}
}
// b.js
import mixinA from './a.js'
export default {
mixins: [mixinA],
created() {
console.log('b')
}
}
// a
// b
复制代码
能够看到调用顺序同官网说明的方式同样。
// a.js
export default {
created() {
console.log('a')
}
}
// b.js
export default {
created() {
console.log('b')
}
}
// index.js
import Vue from 'vue'
import A from './a.js'
import B from './b.js'
const BComponent = Vue.extends('BComponent', B)
B.mixin(A)
new BComponent()
// b
// a
复制代码
调用顺序与在options内挂载mixins方式调用顺序相反,这是为何呢?
这是由于,组件在实例化的初始阶段,必定会与Vue.options进行一次mergeOptions,Vue.options并不存在任何生命周期钩子函数,且mergeOptions内会优先处理组件options上挂载的mixins,因此mixins内的钩子函数会优先被push到对应生命周期hook的第一位,因此在调用的时候,mixins混入组件的生命周期会优先调用。
而方式二中,首先调用了extends与Vue.options进行了merge,拿到了BComponent,BComponent中的hook数组第一位是B组件中的hook函数,以后再mixin其它任何组件都只能排在B组件的hook后面。
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
}
复制代码
props、methods、inject、computed这几个固定都是对象类型(props与inject会被规范化成对象类型),直接进行拷贝赋值便可,同名的选项会被覆盖。在组件内部mixins引入混合组件时,因为宿主组件老是最后处理,因此当混合组件和宿主组件在props、methods、inject、computed中存在同名属性时,会被宿主组件对应的选项覆盖。而调用mixin方法混合的方式则相反,这一点须要注意。
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
}
}
// component、directive、filter
ASSET_TYPES.forEach(function(type) {
strats[type + 's'] = mergeAssets
})
复制代码
乍看之下,directives、filters、components与methods等合并策略很类似,惟独声明res变量的方式不同,mergeAssets中使用的是const res = Object.create(parentVal || null)
而不是直接覆盖,这是为何呢?
咱们知道,vue内部提供了一些内置的指令如v-for、v-if等,和一些内置的组件如KeepAlive、Transition等,咱们能够直接在组件内部使用他们,但奇怪的是,vue并无显式的去注册他们,这实现的关键就在const res = Object.create(parentVal || null)
。
这些内置属性其实就存在Vue.options上,通过mergeAssets处理后会变成:
options = {
...
components: {
...
__proto__: {
...
Transition
}
}
}
复制代码
这样的结构。使用对应的选项时,会顺着对应选项的原型链一层一层向上寻找选项。
实现的很是巧妙,既知足了同名属性'覆盖',又能够内置选项。
// watch挂载的选项不能够直接进行覆盖,须要将每一个选项处理成函数数组形式。
strats.watch = function(
parentVal: ?Object,
childVal: ?Object,
vm?: Component,
key: string
): ?Object {
// Firefox中存在原生的Object.prototype.watch函数
// 为定义watch选项却访问到了watch属性,则重置parentVal与childVal
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
// 若是定义了watch选项
// 将选项的每一个成员处理成数组类型
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
}
复制代码
因为在Firefox中存在原生的Object.prototype.watch函数,在合并watch函数过程当中若是访问到了原生函数,须要作兼容性处理。
合并watch同合并声明周期存在类似之处,都是将选项合并为数组类型。
合并步骤:
转载请注明出处!感谢!