博客更新地址啦~,欢迎访问:https://jerryyuanj.github.io/
上一节可以看成是merge数据前期准备,下面就介绍到了我们的mergeOption
的重头戏啦——merge。先看代码(仍然是mergeOptions里面的代码):
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,再给这个options赋值,最后返回options。这就是mergeOptions方法主要干的事。那么,我们知道,Vue提供的属性很多,比如:el, data, props, filters … 这些属性是怎么来merge的呢?是不是有什么合并的规则,每个属性有自己的对应合并规则呢?
是的。
这段代码有两个for循环,for循环内部都使用了mergeField函数。我们先来看看这个函数:
function mergeField (key) {
const strat = strats[key] || defaultStrat
options[key] = strat(parent[key], child[key], vm, key)
}
该函数接受了一个key作为参数,这个key就是属性名称。然后定义了一个strat
变量,它的值是strats[key] || defaultStrat
,也就是说,会在starts
这个对象中找有没有这个key对应的值,有就用这个值,没有的话就用defaultStrat
作为默认值。所以,这个strats
可以看成我们上面说的合并规则集。它的定义就在当前文件中,我们一点一点来看。
/**
* 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
首先在上面定义了这个对象。它的值是config.optionMergeStrategies
,顺着依赖找到这个定义可以发现他就是一个空对象: Object.create(null)
。所以此时: strats = {}
然后开始给它添加规则了,先是这两个:
/**
* Options with restrictions
*
* el 和 propsData 的合并策略一样
* 注意: 它们只能给使用 new 操作符 创建的 Vue 实例使用
*/
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)
}
}
很简单,就是给 el
和 propsData
设置合并规则——默认的规则。慢着,我们先看一下这个默认合并策略是什么吧,毕竟出现两次了,这也是在当前文件中定义的:
/**
* Default strategy.
*/
const defaultStrat = function (parentVal: any, childVal: any): any {
return childVal === undefined
? parentVal
: childVal
}
默认的策略很简单,就是有孩子用孩子,没孩子用爸爸
。也就是无第二个参数时就使用第一个参数的值。
Ok,继续看
接下来是data的合并规则:
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)
}
首先的判断的 if(!vm)
, 还有内部的两个return,参数列表只少了个vm
,那么有vm和没有vm有什么不一样吗?
首先,不一样是肯定的,现在你可以简单的理解为,没有vm的操作的是子组件。
继续,如果没有提供vm
的话,进入if分支,接着又是一个if:childVal && typeof childVal !== 'function'
,你可能不太了解这个判断,但是你肯定认识这段警告的内容:data选项要是一个函数。是不是很熟悉?
如果满足data是函数的话,则会执行:return mergeDataOrFn(parentVal, childVal)
。当然,如果提供了vm的话,也会执行: mergeDataOrFn(parentVal, childVal, vm)
。那么就来看看这个mergeDataOrFn
的真面目吧。
/**
* Data
*/
export function mergeDataOrFn (
parentVal: any,
childVal: any,
vm?: Component
): ?Function {
if (!vm) {
// in a Vue.extend merge, both should be functions
// 在 Vue.extend 的合并中,两个都应该是函数
if (!childVal) {
return parentVal
}
if (!parentVal) {
return childVal
}
// when parentVal & childVal are both present,
// we need to return a function that returns the
// merged result of both functions... no need to
// check if parentVal is a function here because
// it has to be a function to pass previous merges.
// 当 parentVal 和 childVal 都存在的时候,我们需要返回一个函数,这个函数返回这两个对象
// 合并的结果。这里不需要检查parentVal是不是函数,因为它只有是函数才可以通过之前的合并。
return function mergedDataFn () {
return mergeData(
typeof childVal === 'function' ? childVal.call(this, this) : childVal,
typeof parentVal === 'function' ? parentVal.call(this, this) : parentVal
)
}
} else {
return function mergedInstanceDataFn () {
// instance merge
// 实例上的合并(有vm的情况下)
// 拿到 child 的值,如果是函数,执行它获取返回值,如果不是,直接用值
const instanceData = typeof childVal === 'function'
? childVal.call(vm, vm)
: childVal
// 同理拿到 parent 的值
const defaultData = typeof parentVal === 'function'
? parentVal.call(vm, vm)
: parentVal
// 如果 child 计算结果有值的话,则会返回合并函数,否则直接返回 parent 的计算结果
if (instanceData) {
return mergeData(instanceData, defaultData)
} else {
return defaultData
}
}
}
}
首先,这个函数要么返回mergedDataFn
函数,要么返回mergedInstanceDataFn
,一个给vue实例化使用,一个给 Vue.extend
时使用。
其次我们看到,这两个函数中都返回了mergeData
这个函数,没错,他就是合并data最终要执行的代码:
/**
* Helper that recursively merges two data objects together.
*
* 递归合并两个数据对象
*/
function mergeData (to: Object, from: ?Object): Object {
if (!from) return to
let key, toVal, fromVal
const keys = Object.keys(from)
for (let i = 0; i < keys.length; i++) {
key = keys[i]
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
}
这个函数最终会把值赋到to上,然后返回这个to。这段代码比较简单,就是两个对象的合并。举个例子:
let objA = {
name: 'vue',
author: {
name: 'you',
age: 18
},
home: 'China'
}
let objB = {
name: 'react',
author: {
name: 'facebook',
age: 30
},
version: 12
}
mergeData(objA, objB)
/**
{
author: {name: "you", age: 18}
home: "China"
name: "vue"
version: 12
}
**/
好了,data的合并看完了,继续往下看:
function mergeHook (
parentVal: ?Array<Function>,
childVal: ?Function | ?Array<Function>
): ?Array<Function> {
return childVal
? parentVal
? parentVal.concat(childVal)
: Array.isArray(childVal)
? childVal
: [childVal]
: parentVal
}
LIFECYCLE_HOOKS.forEach(hook => {
strats[hook] = mergeHook
})
这里是生命周期钩子的合并策略,这些钩子定义在 /src/shared/constants.js 中:
export const LIFECYCLE_HOOKS = [
'beforeCreate',
'created',
'beforeMount',
'mounted',
'beforeUpdate',
'updated',
'beforeDestroy',
'destroyed',
'activated',
'deactivated',
'errorCaptured'
]
那么它的合并策略是啥样的呢?我们在代码中只看到这样一行很骚
的代码:
return childVal
? parentVal
? parentVal.concat(childVal)
: Array.isArray(childVal)
? childVal
: [childVal]
: parentVal
乍看很骚,实际上就是三元运算符的嵌套使用,个人觉得这样代码的可读性不好,anyway,我们来看看吧:
childVal ? (parentVal ? parentVal.concat(childVal) : (Array.isArray(childVal) ? childVal : [childVal])): parentVal
也不是很难,加上括号以后就很清晰了。
所以我们最终的生命周期的每个钩子最后都是一个数组,包含着要在对应阶段要执行的操作(回调)
这段逻辑还是比较清晰的,我们继续往下看:
/**
* Assets
*
* When a vm is present (instance creation), we need to do
* a three-way merge between constructor options, instance
* options and parent options.
*
* 当 vm 存在的时候(实例化创建的时候), 我们需要三向合并(构造器的选项,实例选项,父选项)
* constructor options: 我们自己定义的构造函数中的选项
* instance options: 实例选项(原型上的)
* parent options: 传进来的
*/
function mergeAssets (
parentVal: ?Object,
childVal: ?Object,
vm?: Component,
key: string
): Object {
// 以 parentVal 为原型创建一个res对象
const res = Object.create(parentVal || null)
if (childVal) {
// 检查以下 childVal
process.env.NODE_ENV !== 'production' && assertObjectType(key, childVal, vm)
// 实际上也是不同名会赋值,同名会覆盖
return extend(res, childVal)
} else {
return res
}
}
ASSET_TYPES.forEach(function (type) {
strats[type + 's'] = mergeAssets
})
这段代码是给所谓的“资源”定义合并策略,这里的资源实际是在contans.js中定义的:
export const ASSET_TYPES = [
'component',
'directive',
'filter'
]
这个就很简单啦,就是一个extend
的过程。
接下来看看watch的合并对策略:
/**
* Watchers.
*
* Watchers hashes should not overwrite one
* another, so we merge them as arrays.
*
* watcher不应该合并,所以这里把他们合并成数组(回调数组)
*/
strats.watch = function (
parentVal: ?Object,
childVal: ?Object,
vm?: Component,
key: string
): ?Object {
// work around Firefox's Object.prototype.watch...
// 相当于修正 Firefox 上的 watch带来的影响
if (parentVal === nativeWatch) parentVal = undefined
if (childVal === nativeWatch) childVal = undefined
/* istanbul ignore if */
// 没有 childVal 的话, 以 parentVal 为参数创建一个对象后返回
if (!childVal) return Object.create(parentVal || null)
// 有 childVal 的话呢,检查一下是不是对象类型
if (process.env.NODE_ENV !== 'production') {
assertObjectType(key, childVal, vm)
}
// 再检查 parentVal 如果没有值则返回 childVal
if (!parentVal) return childVal
// 走到这里说明 parentVal 和 childVal 都有合法的值了,合并吧
const ret = {}
extend(ret, parentVal)
for (const key in childVal) {
let parent = ret[key] // 获取 parent 的值
const child = childVal[key] // 获取 child 的值
// 如果 parent 有值并且不是数组的话, 把它变成数组
if (parent && !Array.isArray(parent)) {
parent = [parent]
}
// ret[key] 的值由 parent 的值来决定,如果 parent 有值的话,那么就再拼接上 child 的值
// 如果 parent 没有值的话,那么就用 child 的值,不是数组转成数组即可
ret[key] = parent
? parent.concat(child)
: Array.isArray(child) ? child : [child]
}
return ret
}
注释中写的很详细啦,对于 watch
呢,最后它会变成一个数组,这个数组里面注册着父子中定义的所有同属性的回调函数。而且执行顺序是父亲先孩子后
。
再看看其他选项的合并策略吧~
/**
* Other object hashes.
*
* 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
// 两个 extend 的先后顺序可以看到, childVal 的值是覆盖 parentVal的
// 也就是说,如果父子拥有同个key,那么子的值将会作为最终的值
const ret = Object.create(null)
extend(ret, parentVal)
if (childVal) extend(ret, childVal)
return ret
}
// 别漏了这个, provide 的合并策略和 data 一样
strats.provide = mergeDataOrFn
从代码和注释中可以看出来,props, methods, inject, computed这四个对合并策略就比较残暴直接了,孩子会直接覆盖父亲对同名属性。注意哦,这里的props和injects已经在前面被normalize了,所以这里直接操作就行了。可以看出来normalize的一点作用了吧~
其次provide的合并策略就跟data的合并策略是一模一样啦~
至此,我们已经基本分析完了Vue所有选项的合并策略了。有啥感觉呢?是不是感觉懵懵的,就是那种懂但是有说不全的那种感觉?没事,下面我重新用一个例子来解释以下这种合并的结果是怎样的。上一节的例子这里不太适合,因为它没有父子的关系,是直接的实例化。我们这里弄一个父子的关系来举个例子,直观的感受下这些合并策略的结果:
在这里举的例子会涉及一个Vue的很重要的api:Vue.extend
,这里我们不会介绍它的原理,你只要知道它是什么就行了。参考官网的api文档:API - Vue-extend
OK, 我们按照上面介绍的合并规则倒着来,趁热打铁,对于一些相同合并规则的只列举常见的属性来讲解了,先来看看:
props, methods, inject, computed
let Son = Vue.extend({
data () {
return {
name: { firstName: 'Jerry', lastName: 'Yuan' }
}
},
computed: {
fullName(){
return this.firstName + this.lastName
}
},
methods: {
hello(){
console.info('hello from parent')
},
fatherFunc(){
console.info('father function')
}
}
})
let son = new Son({
methods: {
hello(){
console.info('hello from son')
},
sonFunc(){
console.info('son function')
}
},
computed: {
fullName(){
return this.lastName + this.firstName
}
}
})
son.hello()
son.fatherFunc()
son.sonFunc()
console.info(son.fullName)
运行结果:
hello from son
father function
son function
YuanJerry
可以看出来,无论是compute还是methods还是其他没列举的但是应该是表现一致的,同名总是会被孩子option覆盖的。
watch
修改例子:
let Son = Vue.extend({
data () {
return {
name: 'Jerry'
}
},
watch: {
name(){
console.info('name changed detected in parent')
}
}
})
let son = new Son({
watch: {
name(){
console.info('name changed detected in son')
}
}
})
son.name = 'Judy'
输出如下:
name changed detected in parent
name changed detected in son
可见,watch的合并策略如我们所说,是一个回调队列,从父亲的开始执行。而且如果你仔细看源码的话,你会发现,这么写watch也是可以的:
let Son = Vue.extend({
data() {
return {
name: 'Jerry'
}
},
watch: {
name: [
function(){
console.info('name changed parent-01')
},
function(){
console.info('name changed parent-02')
}
]
}
})
let son = new Son({
watch: {
name: [
function() {
console.info('name changed son-01')
},
function(){
console.info('name chaned son-02')
}
]
}
})
son.name = 'Judy'
输出结果:
name changed parent-01
name changed parent-02
name changed son-01
name chaned son-02
是不是有点意思啊,哈哈哈~
assets(directives, filters, components)
hooks
hooks就比较简单了:
let Son = Vue.extend({
created () {
console.info('created in father')
}
})
let son = new Son({
created () {
console.info('created in son')
}
})
这时候就会输出
before created
created in father
created in son
跟watch
有点像,它也可以以数组的形式来定义,这里就不举例子了。另外上面忘了说了,你可以在控制台打印出来看看这些钩子到底是什么样的:
我想你大概清楚了吧~你也可以在上面的例子上打印看看。
let Son = Vue.extend({
data () {
return {
name: {
firstName: 'Jerry'
},
title: 'Back-end',
father: 'I am father'
}
}
})
let son = new Son({
data(){
return {
name: {
lastName: 'Yuan'
},
title: 'Front-end',
son: 'I am son'
}
}
})
我们这里要分析的其实是:son.$options.data
,但是又发现其他两个跟data
貌似有关系的属性:$data 和 _data
,这两个是什么呢?
这里我们先有个印象即可,他们是响应式对象哦。也就是Vue响应式系统对我们定义的data
做的一些很有趣的工作。
好吧,继续回到我们合并策略的分析中~这里我们就看一下最终的data是啥就行了。data不是函数吗?执行完就可以获取到值了:
这个函数的名称是mergedInstanceDataFn
,最终的data数据呢,把name的firstName和lastName合并到name中去了,title覆盖了父亲定义的,然后father和son都保留着。
嗯~OK
let Son = Vue.extend({})
let son = new Son({
el: '#app',
propsData: {
name: 'joker',
title: 'FE Dev'
}
})
结果就是这样子:
son.$options.el // #app
son.$options.propsData // { name: 'joker', title: 'FE Dev'}
Ok,是不是明朗许多了。
现在我们结合上一节一开始的一个简单的例子,结合我们的合并策略,看看发生了什么:
var app = new Vue({
el: '#app',
data(){
return {
name: 'hello'
}
}
})
接本节一开始的内容:
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
这里我们先明确一下,parent, child, vm 都是在这里传入的(/src/core/instance/init.js的_init方法)
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
第一个参数resolveConstructorOptions(vm.constructor)
我们之前也说了,在这个例子中,它就是Vue.options
,而这个对象我们之前vue初始化都干了什么 已经分析过了:
{components: {keepAlive}, directives: {}, filters: {}, _base: Vue}
所以,这个就是我们的parent了吗?答案是不全是的!
我们在之前vue初始化都干了什么 这一节的一开始分析src目录的时候就已经说了,vue是多平台的,这时候应该是最内层的vue暴露出来,然后给对应的平台再包装一下(这里的平台是web)。所以,在vue的runtime时候,也会给vue添加一些东西,这里就包括了在Vue.options上添加的directives
和components
。具体的代码逻辑呢,可以参考这两句(/src/platforms/web/runtime/index.js):
// install platform runtime directives & components
extend(Vue.options.directives, platformDirectives)
extend(Vue.options.components, platformComponents)
各自对应的值分别是:
指令(directives)
{
Transition,
TransitionGroup
}
组件:
{
model,
show
}
这也就是我们在使用vue的时候,为什么可以在任何组件中使用v-show
,
等内置功能的原因。
OK,所以我们这里的Vue.options,也就是本例中的parent完整的应该是:
{
components: {KeepAlive, Transition, TransitionGroup},
directives: {model, show},
filters: {},
_base: Vue
}
而child就是我们定义的options啦,就是它:
{
el: '#app',
data(){
return {
name: 'hello'
}
}
}
也就是:
vm.$options = mergeOptions(
{
components: {KeepAlive, Transition, TransitionGroup},
directives: {model, show},
filters: {},
_base: Vue
},
{
el: '#app',
data(){
return {
name: 'hello'
}
}
},
vm
)
然后就交给mergeOptions
啦,就是我们上一节介绍那么多的,你现在可以带着这个去回顾上一节加深一下印象也行~~
Ok,言归正传,这节的主要内容是合并策略,那你看,我们上面分析了那么多属性的合并策略,这里应该就很简单了吧。都在我们的分析范围内。
so,我们来看看这个$options的庐山真面目吧:
是不是很简单啦~也没什么复杂的(关于render
,staticRenderFns
我们之后分析到编译部分再说。)