0.前情提要:
Vue.prototype._init = function (options?: Object) {
// ...
vm.$options = mergeOptions(
// 该函数会沿着原型链往上找到有options属性的地方
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
// ...
}
之前已经看到new Vue的时候是在执行
_init方法,在_init中合并了传入的options,本文具体看看options是如何被合并的
1.mergeOptions函数
找到core/instance/util/options.js,大概做了以下工作
- 检测名称
name - 统一
options输入属性的类型(props,inject,directives) - 将
extends,mixins递归合并 - 按照定义好的不同属性的合并策略进行合并
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
}
// 将props声明 统一格式化为对象{[key: string]:{type: 'string',default?:any}}
normalizeProps(child, vm)
// 和props差不多的逻辑
normalizeInject(child, vm)
// 直接声明函数的时候转化为{bind: def,update: def}
normalizeDirectives(child)
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.1 格式化 props、inject、directives
normalizePropsprops的声明有两种方式
- 数组声明 如
props: ['a','b'],元素是props属性名 - 对象声明 如
props:{a:{type:"string",default},b:"string"}最终转换的格式应该是{[key: string]:{type: string,default?: any}}
// 简化后的
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]
// w-w 转为驼峰命名
name = camelize(val)
// 增加类型校验
res[name] = { type: null }
}
// 对象形式的声明
} else if (isPlainObject(props)) {
for (const key in props) {
val = props[key]
// w-w 转为驼峰命名
name = camelize(key)
res[name] = isPlainObject(val)
? val
// 对象声明的时候,如果属性值不是对象,那么属性值就代表类型
: { type: val }
}
}
options.props = res
}
normalizeInject
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 }
}
}
}
normalizeDirectives注册组件可以直接赋值函数,也可以传入一个对象,里面的属性是一些钩子函数
,格式化的目的就是转化为一个包含钩子函数的对象
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 合并child中的extends、mixins
一个mergeOptions的递归调用,合并之后修改parent
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)
}
}
}
1.3 合并child和parent的并集
不同的属性有不同的合并策略,列举一下
el、propsDatadata- 各个生命周期
hook(来自LIFECYCLE_HOOKS) componets、directive、filter来自(ASSET_TYPES)watchprops、methods、inject、computedprovide
el、propsData(合并覆盖)
实际上就是用的默认合并策略,只不过在开发环境下做了容错警告
if (process.env.NODE_ENV !== 'production') {
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)
}
}
那么看看defaultStrat,就是如果子集存在,把父级覆盖,这里其实就可以回想到extends和mixins的合并,因为合并之后赋给了parent此时在合并属性的时候,extends和mixins中的属性会被child中的覆盖(这里指的是用默认合并策略的时候)
const defaultStrat = function (parentVal: any, childVal: any): any {
return childVal === undefined
? parentVal
: childVal
}
data(对象的递归合并覆盖)
strats.data = function (
parentVal: any,
childVal: any,
vm?: Component
): ?Function {
if (!vm) {
// 要求child的data属性是一个函数
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)
}
mergeDataOrFn
export function mergeDataOrFn (parentVal: any,childVal: any,vm?: Component): ?Function {
if (!vm) {
// 两个中有一个不存在就返回另一个
if (!childVal) {
return parentVal
}
if (!parentVal) {
return childVal
}
return function mergedDataFn () {
return mergeData(
typeof childVal === 'function' ? childVal.call(this, this) : childVal,
typeof parentVal === 'function' ? parentVal.call(this, this) : parentVal
)
}
} else {
// 考虑到data属性是对象或函数的情况
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
}
}
}
}
mergeData
这里有种情况说明一下,如果parent和child的值不全是对象,那么就会child覆盖parent,因为没法合并
function mergeData (to: Object, from: ?Object): Object {
if (!from) return to
let key, toVal, fromVal
// 获取到父data的属性名
const keys = hasSymbol
? Reflect.ownKeys(from)
: Object.keys(from)
// 对父data的属性做循环
for (let i = 0; i < keys.length; i++) {
// 获取属性名
key = keys[i]
// 跳过__ob__
if (key === '__ob__') continue
toVal = to[key]
fromVal = from[key]
if (!hasOwn(to, key)) {
set(to, key, fromVal)
} else if (
// 属性值都是对象,递归合并
toVal !== fromVal &&
isPlainObject(toVal) &&
isPlainObject(fromVal)
) {
mergeData(toVal, fromVal)
}
}
return to
}
hook(数组合并,先父后子,去重)
LIFECYCLE_HOOKS.forEach(hook => {
strats[hook] = mergeHook
})
// shared/constant.js
export const LIFECYCLE_HOOKS = [
'beforeCreate',
'created',
'beforeMount',
'mounted',
'beforeUpdate',
'updated',
'beforeDestroy',
'destroyed',
'activated',
'deactivated',
'errorCaptured',
'serverPrefetch'
]
mergeHook
这个三目运算符嵌套看着有点晕,还是解释一下吧
child不存在,那直接用`parentchild存在,parent不存在,那就用child(如果child不是数组就强行变数组[child])child存在,parent也存在,parent直接连接child(parent.concat(child))注意这里concat既可以连接数组,也能连接单个元素,这样的连接方式也决定了调用顺序,先父后子
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
}
dedupeHooks去重
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
}
componets、directive、filter(合并覆盖)
ASSET_TYPES.forEach(function (type) {
strats[type + 's'] = mergeAssets
})
// shared/constant.js
export const ASSET_TYPES = [
'component',
'directive',
'filter'
]
mergeAssets
function mergeAssets (parentVal: ?Object,childVal: ?Object,vm?: Component,key: string): Object {
// 把parent放到原型链上
const res = Object.create(parentVal || null)
if (childVal) {
// 规定传值类型为对象
process.env.NODE_ENV !== 'production' && assertObjectType(key, childVal, vm)
return extend(res, childVal)
} else {
return res
}
}
// shared/util.js
export function extend (to: Object, _from: ?Object): Object {
for (const key in _from) {
to[key] = _from[key]
}
return to
}
watch(合并覆盖,强制数组封装)
strats.watch = function (parentVal: ?Object,childVal: ?Object,vm?: Component,key: string): ?Object {
// 好像是火狐的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 = {}
// 都存在就合并覆盖
// 每一个watch属性下都是数组
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
}
props、methods、inject、computed(合并覆盖)
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
}
provide(同data)
strats.provide = mergeDataOrFn
2.简单总结
- 说是和
parent和child的合并,实际上很重要的一点是mixins和extends合并也已经在这里进行了,而且mixins的合并规则好像经常被单独拎出来会出面试 - 合并的过程,伴随着数据结构的格式化(通一个属性,在使用的时候允许多种数据类型输入方式,但是内部处理的时候肯定是要转换统一的),这个应该为了后面的处理做准备的。
- 有意思的是创建对象的时候用
Object.creat(null),这样会省去Object原型链的挂载,比字面量创建对象看起来简洁舒服,学到了~ 4.通过strats对象定义不同的合并策略实际上就是用了设计模式里的策略模式
下一节可能会根据
_init函数initState函数进行阅读,未完待续~