Vue源码解析三——选项合并

上一章Vue源码解析二——从一个小例子开始逐步分析看完规范化选项之后,再来看看合并阶段是如何处理的,接下来是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

看这段代码的开头和结尾可知,mergeOptions函数最后是返回了一个新的对象,中间的代码就是充实这个新对象的。

两个for in循环分别遍历了parent和child,并以对象的键为参数调用了mergeField函数。在遍历child的循环中多了一个判断,如果在parent中出现过的键就不再进行处理了。

在我们之前所举的例子中,parent就是Vue.options, child就是我们实例化时传过来的参数。

// parent
Vue.options = {
  components: {
      KeepAlive,
      Transition,
      TransitionGroup
  },
  directives: {
      model,
      show
  },
  filters: Object.create(null),
  _base: Vue
}

// child
{
  el: '#app',
  data: { a: 1 }
}

所以第一个循环的key分别是:componentsdirectivesfiltersfilters

第二个循环的key分别是:eldata

这两个循环都调用了mergeField函数,所以真正的实现在该函数中:

function mergeField (key) {
  const strat = strats[key] || defaultStrat
  options[key] = strat(parent[key], child[key], vm, key)
}

该函数先是定义了strat常量,它的值是是根据参数key得到的strats里面的值,没有的话就是defaultStrat. 然后以parent[key], child[key], vm, key为参数调用了strat,并把返回值赋给了options[key]. 所以我们还是要先弄清楚strats是什么。这就要从core/util/options.js文件的开头看起了。

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

这句代码就定义了strats常量,它的值是config.optionMergeStrategies, 来自于core/config.js文件。现在它的值还是一个空对象。我们翻译一下上面的注释:

选项覆盖策略是处理如何将父选项值和子选项值合并到最终值的函数。
所以config.optionMergeStrategies是一个包含策略函数的对象,接下来就是要根据不同的选项定义不同的策略函数了。

选项 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)
  }
}

在非生产环境下添加了elpropsData两个属性,也就是用来处理el和propsData的选项的合并的,值是一个函数,来看一下这个函数的内容。

先是一个if判断,如果没有vm参数,会发出一个警告:提示elpropsData选项只能用在用new创建实例的时候。

否则直接调用defaultStrat函数并返回,该函数的定义也在options.js文件中:

/**
 * Default strategy.
 */
const defaultStrat = function (parentVal: any, childVal: any): any {
  return childVal === undefined
    ? parentVal
    : childVal
}

根据注释可知,它是默认的策略函数。函数体也很简单,就是判断childVal是否存在,如果存在就返回childVal,否则返回parentVal。

选项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)
}

该策略函数用来合并处理data选项。首先也是一个if语句判断是否传递了vm参数,那么这个vm参数代表什么呢?我们是在mergeOptions函数中看到mergeField里面有调用才来到这里看策略函数的:

function mergeField (key) {
    const strat = strats[key] || defaultStrat
    options[key] = strat(parent[key], child[key], vm, key)
}

传给strat的vm参数是mergeOptions接收的参数,这个参数从何而来呢?是我们在_init函数中调用mergeOptions时传进来的

const vm: Component = this
vm.$options = mergeOptions(
    resolveConstructorOptions(vm.constructor),
    options || {},
    vm
)

所以vm就是Vue实例。

既然判断了vm是否存在,那什么时候没有传递vm参数呢?在Vue.extend方法中也有调用mergeOptions函数,这里就没有传递vm参数:

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

我们知道子组件是通过实例化Vue.extend创造的子类实现的,也就是说子组件的实例化不会传递vm参数。

接着看if里面的内容:

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)

如果存在childVal(即data选项)并且它不是一个函数,就会在非生产环境下发出警告:data选项必须是一个函数,并且直接返回parentVal。也就是说子组件的data选项必须是一个函数

否则调用mergeDataOrFn函数并返回其执行结果。

如果vm存在,也是返回mergeDataOrFn的执行结果,但是会多传一个vm参数。

既然如论是子组件的data选项还是非子组件的选项都调用了mergeDataOrFn函数,我们就来看看该函数吧

export function mergeDataOrFn (
  parentVal: any,
  childVal: any,
  vm?: Component
): ?Function {
  if (!vm) {
    ...
  } else {
    ...
}

代码整体结构就是if else判断,if里面是处理子组件选项的,我们先看一下:

// in a Vue.extend merge, both should be functions
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.
return function mergedDataFn () {
    return mergeData(
        typeof childVal === 'function' ? childVal.call(this, this) : childVal,
        typeof parentVal === 'function' ? parentVal.call(this, this) : parentVal
    )
}

最上面的一句注释是说在Vue.extend合并处理中,父子data选项都应该是一个函数

下面是两个if判断:如果子选项不存在就返回父选项,如果父选项不存在就返回子选项。

如果都存在继续执行,最后返回一个mergedDataFn函数,里面是mergeData的返回结果,但是它还没有执行,因为mergedDataFn函数还没有执行。

接下来再看else语句,也就是处理非子组件的内容:

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

直接返回了mergedInstanceDataFn函数。也就是说不管是不是子组件,mergeDataOrFn都返回一个函数,即data选项始终是一个函数。

  1. 直接创建Vue实例
mergeOptions1.png
  1. 合并子组件选项 —— 没有父选项有子选项
mergeOptions2.png
  1. 合并子组件选项 —— 没有子选项有父选项
mergeOptions3.png
  1. 合并子组件选项 —— 父子选项都有
mergeOptions4.png

回头再看一下这两个函数里面都调用了mergeData函数,返回的也是它的调用结果,所以真正合并data选项的处理在该函数中。在看函数实现之前,先看看该函数接收的参数。

无论是mergedDataFn还是mergedInstanceDataFn函数都有如下对父子选项的处理:

typeof childVal === 'function' ? childVal.call(this, this) : childVal,
typeof parentVal === 'function' ? parentVal.call(this, this) : parentVal

mergeData函数接收的参数正是这个处理之后的结果,也就是两个纯对象。接下来再看函数实现:

/**
 * 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 = 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...
    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
}

根据函数上方的注释我们知道该函数的功能是将两个对象合并到一起,函数最后返回to,可知是将from的属性混合到了to对象上。根据调用函数时的传参顺序可知,tochildVal产生的纯对象,fromparentVal产生的纯对象。在看一下函数实现

  • 如果from不存在,直接返回to
  • 循环遍历from的属性,如果属性不在to对象中,则在to对象上设置对应的属性和值
  • 如果属性在to对象中,并且toValfromVal都是对象,则递归调用mergeData对属性值进行深度合并

到这里,data选项的策略函数我们就看完了

生命周期钩子选项的策略合并

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

LIFECYCLE_HOOKS定义在shared/constants.js文件中:

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

正是我们所熟知的生命周期钩子。循环遍历了LIFECYCLE_HOOKS数组,以遍历hook为键mergeHook函数为值添加到了strats策略对象中。所有的钩子函数都是相同的策略函数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
}

根据函数上方的注释可知最后被合并成了数组。函数体中res的结果由三组三木运算所得

  • 如果子选项不存在直接等于父选项的值
  • 如果子选项存在,再判断父选项。如果父选项存在, 就把父子选项合并成一个数组
  • 如果父选项不存在,判断子选项是不是一个数组,如果不是数组将其作为数组的元素

如果子选项不存在直接返回父选项的值,父选项一定是数组吗?我们来看这样一个例子:

var Pub = Vue.extend({
  beforeCreate: function () {
    console.log('pub')
  }
})

var Sub = Pub.extend({})

输出Sub.options.beforeCreate的值
[{
  beforeCreate: function () {
    console.log('pub')
  }
}]

调用Vue.extend时在mergeOptions执行钩子函数的合并策略时,parentVal是Vue.options.beforeCreate是undefined,childVal是beforeCreate: function () { console.log('pub') }, childVal存在parentVal不存在执行这个:

Array.isArray(childVal)
        ? childVal
        : [childVal]

所以最后Pub.options.beforeCreate的值是:

Pub.options.beforeCreate = [{
  beforeCreate: function () {
    console.log('pub')
  }
}]

而Pub.extend({})执行合并策略时,parentVal的值是

[{
  beforeCreate: function () {
    console.log('pub')
  }
}]

childVal的值是undefined,直接返回父选项,的确结果是一个数组。

当parentVal有值时执行

parentVal.concat(childVal)

根据前面的分析我们知道parentVal是一个数组,所以可以调用concat方法。

mergeHook函数的最后是这样一段代码:

return res
    ? dedupeHooks(res)
    : res

res有值则以它为参数调用dedupeHooks函数,返回值作为最终结果返回,否则直接返回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
}

这个函数的作用主要是把重复数据删除(不过感觉没用啊。。)

资源(assets)选项的合并策略

ASSET_TYPES.forEach(function (type) {
  strats[type + 's'] = mergeAssets
})

都有哪些资源? 我们看一下ASSET_TYPES的值是什么。该常量定义在shared/constants.js文件中:

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

在循环中又给每个元素后面加了字符s,所以Vue中的资源是componentsdirectivesfilters。我们再来看一下合并资源的策略函数mergeAssets:

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

这段代码逻辑很简单,首先是以parentVal为原型创建对象 res, 然后判断是否存在childVal, 如果有的话调用extend()childVal合并到res上,没有就直接返回res。

我们可以在任意的组件中使用像KeepAlive这种内置组件,就是因为这句

Object.create(parentVal || null)

在合并资源的时候把Vue内置组件放到了原型上,这样即使子组件中没有注册这样的组件,也会寻着原型链查找,找到了就可以使用。比如:

var vm = new Vue({
    components: {
        testA: { data() {}}
    }
})

我们看一下vm.$options.components的打印结果:

components.png

选项watch的合并策略

/**
 * Watchers.
 *
 * Watchers hashes should not overwrite one
 * another, so we merge them as arrays.
 */
strats.watch = function (
  parentVal: ?Object,
  childVal: ?Object,
  vm?: Component,
  key: string
): ?Object {
  // work around Firefox's Object.prototype.watch...
  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
  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
}

先看看这两句:

// work around Firefox's Object.prototype.watch...(解决Firefox的Object.prototype.watch)
if (parentVal === nativeWatch) parentVal = undefined
if (childVal === nativeWatch) childVal = undefined

在Firefox浏览器中Object.prototype拥有原生的watch属性,所以即使我们实例化时没有传递watch属性,在FireFox中也能获取到watch属性,这就会在处理时造成困扰。所以这里的判断是如果选项是原生watch,就把选项置为undefined,就不用处理了。nativeWatch定义在core/util/env.js 文件中:

// Firefox has a "watch" function on Object.prototype...
export const nativeWatch = ({}).watch

在 Firefox 中时 nativeWatch 为原生提供的函数,在其他浏览器中 nativeWatch 为 undefined.

接下来是三个if判断:

if (!childVal) return Object.create(parentVal || null)
if (process.env.NODE_ENV !== 'production') {
    assertObjectType(key, childVal, vm)
}
if (!parentVal) return childVal
  • 如果不存在子选项,直接返回以父选项为原型创建的对象
  • 子选项存在,在非生产环境下判断子选项是否是纯对象
  • 如果父选项不存在,直接返回子选项

接下来就是父子选项都存在的处理:

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) // 如果parent存在,直接合并父选项和子选项中当前属性的值
      : Array.isArray(child) ? child : [child] // 不存在,将child转成数组返回
}
return ret

总之就是把子选项的每一项变成数组返回了。

所以watch选项下的每一项,有可能是函数(当父选项不存在时),可能是数组(父子选项都存在)

选项 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
}
  • 首先是当子选项存在并且在非生产环境下时,检测子选项是不是纯对象
  • 如果父选项不存在,直接返回父选项
  • 先是以null为原型创建对象ret,然后合并retparentVal. 如果子选项存在,再将childVal的属性混合到ret中,这个时候子选项将会覆盖父选项都同名属性
  • 最后返回ret

选项 provide 的合并策略

strats.provide = mergeDataOrFn

这个很简单,只有一句代码,使用函数mergeDataOrFn, 这个函数我们在data的策略函数中看到过了。

至此选项的合并已经看完了,之后我们再接着往下看。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,013评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,205评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,370评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,168评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,153评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,954评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,271评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,916评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,382评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,877评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,989评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,624评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,209评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,199评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,418评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,401评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,700评论 2 345

推荐阅读更多精彩内容

  • 这篇笔记主要包含 Vue 2 不同于 Vue 1 或者特有的内容,还有我对于 Vue 1.0 印象不深的内容。关于...
    云之外阅读 5,045评论 0 29
  • 回忆 首先,render函数中手写h=>h(app),new Vue()实例初始化init()和原来一样。$mou...
    LoveBugs_King阅读 2,267评论 1 2
  • Vue 实例 属性和方法 每个 Vue 实例都会代理其 data 对象里所有的属性:var data = { a:...
    云之外阅读 2,201评论 0 6
  • vue概述 在官方文档中,有一句话对Vue的定位说的很明确:Vue.js 的核心是一个允许采用简洁的模板语法来声明...
    li4065阅读 7,187评论 0 25
  • ORA-00001: 违反唯一约束条件 (.) 错误说明:当在唯一索引所对应的列上键入重复值时,会触发此异常。 O...
    我想起个好名字阅读 5,176评论 0 9