Vue源码阅读(28):mergeOptions() 方法源码解析

 我的开源库:

今天和大家讲讲在 Vue 源码中很重要的一个方法 — mergeOptions(),该方法的作用是合并 options,主要在以下三个地方会被使用到:

  1. Vue 实例初始化时,用于构造函数 options 和当前实例 options 的合并。
  2. Vue.mixin 全局方法内部直接调用 mergeOptions() 方法实现功能。
  3. Vue.extend 全局方法内部会调用 mergeOptions() 实现父级 options 和传递配置 options 的合并。

这里所说的 options 存在于 Vue 构造函数中,保存着一些资源,例如:data、watch、methods、filters、props、生命周期函数等。

options 除了存在于构造函数中,我们在 new Vue({}) 时传递的对象、Vue.mixin({}) 传递的对象、Vue.extend({}) 传递的对象也都是 options。

mergeOptions() 方法就用于合并上面所说的这些对象。在上面的第一种情况中,合并完成的对象会被赋值到 Vue 实例的 $options 属性上,$options 属性保存着对应 Vue 实例能够使用的各种资源,该属性会在后续的实例初始化被用到,关于 Vue 实例的初始化我会在后续单独写一篇文章进行解析。至于第二种和第三种情况,则会将传递进来的 options 和构造函数的 options 进行合并,并将合并完成的对象重新赋值到构造函数的 options 属性上,这能够实现一种全局注入的效果,导致后续通过该构造函数生成的 Vue 实例能够使用这些全局注入的资源。

mergeOptions() 方法定义在 src/core/util/options.js 文件中,我们开始看源码。

1,mergeOptions() 方法总览

首先总览一下 mergeOptions() 方法的全部源码,接下来,我们一步步进行源码解析。

/**
 * 将两个 options 对象合并成一个
 */
export function mergeOptions (
  parent: Object,
  child: Object,
  vm?: Component
): Object {
  if (process.env.NODE_ENV !== 'production') {
    // 对配置对象的 components 字段进行检测
    checkComponents(child)
  }

  if (typeof child === 'function') {
    child = child.options
  }

  // 标准化 child 中的 props、inject、directives
  normalizeProps(child, vm)
  normalizeInject(child, vm)
  normalizeDirectives(child)

  // 对应官方文档点击这里:https://cn.vuejs.org/v2/api/#extends
  // 配置选项中可以使用 extends 配置项,如果使用了该配置项的话,底层则递归调用 mergeOptions 方法,
  // 对 parent 和 extendsFrom 中的配置项进行合并
  const extendsFrom = child.extends
  if (extendsFrom) {
    parent = mergeOptions(parent, extendsFrom, vm)
  }
  // 对应官方文档点击这里:https://cn.vuejs.org/v2/api/#mixins
  // 配置选项中可以使用 mixins 配置项,如果使用了该配置项的话,底层则递归调用 mergeOptions 方法,
  // 对 parent 和 child.mixins[i] 中的配置项进行合并
  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) {
    // 如果当前遍历的 key 在 parent 中不存在的话,再执行 mergeField(key)
    if (!hasOwn(parent, key)) {
      mergeField(key)
    }
  }
  // 用于合并某一个 key 的方法
  function mergeField (key) {
    // strats 是指合并策略集
    // strat 是指特定选项的合并策略,是一个函数
    const strat = strats[key] || defaultStrat
    // 使用这个合并策略对 parent 和 child 中指定的 key 进行合并
    options[key] = strat(parent[key], child[key], vm, key)
  }
  return options
}

2,检查配置项 components 中的组件名是否规范

export function mergeOptions (
  parent: Object,
  child: Object,
  vm?: Component
): Object {
  if (process.env.NODE_ENV !== 'production') {
    // 对配置对象的 components 字段进行检测
    checkComponents(child)
  }

  ......
  ......
}

检查组件名称是否规范使用了 checkComponents 方法,我们看下该方法的源码。

/**
 * 验证组件名称
 */
function checkComponents (options: Object) {
  // 对配置对象的 components 字段进行遍历
  for (const key in options.components) {
    const lower = key.toLowerCase()
    // 判断组件是不是 Vue 中内置的组件(slot,component)
    // 判断组件是不是 HTML 预留的标签
    // 如果满足的话,打印出警告
    if (isBuiltInTag(lower) || config.isReservedTag(lower)) {
      warn(
        'Do not use built-in or reserved HTML elements as component ' +
        'id: ' + key
      )
    }
  }
}

使用 for in 遍历 options.components 拿到组件的名称,然后将组件的名称都变成小写的形式,最后调用 isBuiltInTag() 方法和 isReservedTag() 方法判断组件名是否规范,如果不规范的话,则打印出警告。

isBuiltInTag() 方法用于判断组件名是不是 Vue 内置的标签,源码如下:

/**
 * Check if a tag is a built-in tag.
 */
export const isBuiltInTag = makeMap('slot,component', true)

isReservedTag() 用于判断组件名是不是 HTML 预留的标签,源码如下:

export const isHTMLTag = makeMap(
  'html,body,base,head,link,meta,style,title,' +
  'address,article,aside,footer,header,h1,h2,h3,h4,h5,h6,hgroup,nav,section,' +
  'div,dd,dl,dt,figcaption,figure,picture,hr,img,li,main,ol,p,pre,ul,' +
  'a,b,abbr,bdi,bdo,br,cite,code,data,dfn,em,i,kbd,mark,q,rp,rt,rtc,ruby,' +
  's,samp,small,span,strong,sub,sup,time,u,var,wbr,area,audio,map,track,video,' +
  'embed,object,param,source,canvas,script,noscript,del,ins,' +
  'caption,col,colgroup,table,thead,tbody,td,th,tr,' +
  'button,datalist,fieldset,form,input,label,legend,meter,optgroup,option,' +
  'output,progress,select,textarea,' +
  'details,dialog,menu,menuitem,summary,' +
  'content,element,shadow,template,blockquote,iframe,tfoot'
)

// this map is intentionally selective, only covering SVG elements that may
// contain child elements.
export const isSVG = makeMap(
  'svg,animate,circle,clippath,cursor,defs,desc,ellipse,filter,font-face,' +
  'foreignObject,g,glyph,image,line,marker,mask,missing-glyph,path,pattern,' +
  'polygon,polyline,rect,switch,symbol,text,textpath,tspan,use,view',
  true
)

export const isReservedTag = (tag: string): ?boolean => {
  return isHTMLTag(tag) || isSVG(tag)
}

3,标准化 props、inject、directives

export function mergeOptions (
  parent: Object,
  child: Object,
  vm?: Component
): Object {
  ......
  ......

  // 标准化 child 中的 props、inject、directives
  normalizeProps(child, vm)
  normalizeInject(child, vm)
  normalizeDirectives(child)

  ......
  ......
}

这三个函数所做的事情很简单,就是将 options 中配置的 props、inject、directives 进行标准化,将简略的配置转换成完备的对象形式,例如:

3-1,将 props: ['age'] 转换成

props: {
  age: {
    type: Number,
    default: 0,
    required: true,
    validator: function (value) {
      return value >= 0
    }
  }
}

3-2,将 inject: ['foo'] 转换成

inject: {
  foo: {
    from: 'bar',
    default: () => [1, 2, 3]
  }
}

3-3,将 directives 的函数简写形式转换成对象形式

directives: {
  'color-swatch': function (el, binding) {
    el.style.backgroundColor = binding.value
  }
}

转换成

directives: {
  'color-swatch': {
    bind: function (el, binding) {
      el.style.backgroundColor = binding.value
    },
    update: function (el, binding) {
      el.style.backgroundColor = binding.value
    }
  }
}

4,extends 和 mixins 选项的处理

这部分看注释即可。

export function mergeOptions (
  parent: Object,
  child: Object,
  vm?: Component
): Object {
  ......
  ......

  // 对应官方文档点击这里:https://cn.vuejs.org/v2/api/#extends
  // 配置选项中可以使用 extends 配置项,如果使用了该配置项的话,底层则递归调用 mergeOptions 方法,
  // 对 parent 和 extendsFrom 中的配置项进行合并
  const extendsFrom = child.extends
  if (extendsFrom) {
    parent = mergeOptions(parent, extendsFrom, vm)
  }
  // 对应官方文档点击这里:https://cn.vuejs.org/v2/api/#mixins
  // 配置选项中可以使用 mixins 配置项,如果使用了该配置项的话,底层则递归调用 mergeOptions 方法,
  // 对 parent 和 child.mixins[i] 中的配置项进行合并
  if (child.mixins) {
    for (let i = 0, l = child.mixins.length; i < l; i++) {
      parent = mergeOptions(parent, child.mixins[i], vm)
    }
  }

  ......
  ......
}

5,开始真正的合并处理

export function mergeOptions (
  parent: Object,
  child: Object,
  vm?: Component
): Object {
  ......
  ......

  // 开始进行真正的合并逻辑
  const options = {}
  let key
  for (key in parent) {
    mergeField(key)
  }
  for (key in child) {
    // 如果当前遍历的 key 在 parent 中不存在的话,再执行 mergeField(key)
    if (!hasOwn(parent, key)) {
      mergeField(key)
    }
  }
  // 用于合并某一个 key 的方法
  function mergeField (key) {
    // strats 是指合并策略集
    // strat 是指特定选项的合并策略,是一个函数
    const strat = strats[key] || defaultStrat
    // 使用这个合并策略对 parent 和 child 中指定的 key 进行合并
    options[key] = strat(parent[key], child[key], vm, key)
  }
  return options
}

首先创建一个 options 对象,这个对象就是最终返回的合并对象。

接下来遍历 parent options 对象,以遍历到的 key 作为参数执行 mergeField 方法,这样的话,parent options 对象中的所有 key 都已经处理到 options 对象中了。

然后遍历 child options 对象,这一遍遍历是为了处理 child options 对象中独有的 key,内部的处理也是以 key 作为参数执行 mergeField 方法。

上面两处都使用到了 mergeField 方法,我们细致研究一下。

5-1,mergeField() 方法

// 用于合并 parent options 和 child options 对象中某一个 key 的方法
function mergeField (key) {
  // strats 是指合并策略集
  // strat 是指特定选项的合并策略,是一个函数
  const strat = strats[key] || defaultStrat
  // 使用这个合并策略对 parent 和 child 中指定的 key 进行合并
  options[key] = strat(parent[key], child[key], vm, key)
}

该方法的作用是:用于合并 parent options 和 child options 对象中某一个 key。

在 Vue 中,不同的配置项有不同的合并策略,这些合并策略都存储在 strats 对象中,strats 对象的数据结构如下所示:

strats = {
  data: function(){},
  watch: function(){},
  props: function(){},
  methods: function(){},
  created: function(){},
  mounted: function(){},
  ......
}

该对象中的 key 对应 options 中的配置项,value 是一个个函数,这些函数用于具体配置项的合并。

在 mergeField 方法中,首先根据 key 从 strats 对象中获取指定的合并策略函数,然后调用合并策略函数合并 parent[key] 和 child[key],并将合并的结果赋值到 options[key]。

为了更好的理解,我们看下生命周期配置选项的合并策略函数。

5-2,生命周期配置选项的合并策略函数

生命周期配置选项的合并策略函数是 mergeHook() 方法,虽然一眼看上去很复杂,使用了嵌套的三元表达式,但是如果仔细看的话,其实很简单,详细的源码解释都在注释中,看注释即可理解。

/**
 * Hooks and props are merged as arrays.
 */
function mergeHook (
  parentVal: ?Array<Function>,
  childVal: ?Function | ?Array<Function>
): ?Array<Function> {
  // 嵌套的三元运算符
  // 如果 childVal 没有定义的话,直接返回 parentVal
  return childVal
    // childVal 是定义了的,接下来看 parentVal 是否定义
    ? parentVal
      // parentVal 定义了的话,使用 concat 函数连接两者,concat 函数的参数既能是数组,也能是单个的元素
      ? parentVal.concat(childVal)
      // childVal 定义了,而 parentVal 没有定义,此时只要返回 childVal 即可
      // 不过要看 childVal 是否是数组类型。如果是的话,直接返回,不是的话,包装成数组再返回
      : Array.isArray(childVal)
        ? childVal
        : [childVal]
    : parentVal
  // 这种返回结果由两种变量决定,每个变量各有两种变化,共有四种组合的情形可以使用三元运算符简洁的实现
  // 很值得我们借鉴和学习
}

// 生命周期函数数组
// const LIFECYCLE_HOOKS = [
//   'beforeCreate',
//   'created',
//   'beforeMount',
//   'mounted',
//   'beforeUpdate',
//   'updated',
//   'beforeDestroy',
//   'destroyed',
//   'activated',
//   'deactivated',
//   'errorCaptured'
// ]
// 生命周期函数的合并策略
LIFECYCLE_HOOKS.forEach(hook => {
  strats[hook] = mergeHook
})

6,结语

mergeOptions() 方法就讲到这,下一篇文章解析 Vue.extend() 的内部实现原理。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,让我来回答你的问题。 Vue 组件 object_vue 码系列一是关于 Object.defineProperty 的。Object.defineProperty 是 JavaScript 中的一个方法,可以用来定义对象的属性。这个方法可以让我们定义一个新的属性或者修改一个已经存在的属性。这个方法的语法如下: ```javascript Object.defineProperty(obj, prop, descriptor) ``` 其中,obj 是要定义属性的对象,prop 是要定义或修改的属性名,descriptor 是属性的描述符,它是一个对象,可以包含以下属性: - value:属性的值,默认为 undefined。 - writable:属性是否可写,默认为 false。 - enumerable:属性是否可枚举,默认为 false。 - configurable:属性是否可配置,默认为 false。 使用 Object.defineProperty 方法,可以实现一些高级的对象操作,例如: 1. 将一个属性设置为只读,即无法修改。 2. 将一个属性设置为不可枚举,即无法通过 for...in 循环遍历到该属性。 3. 将一个属性设置为不可配置,即无法删除该属性或者修改该属性的描述符。 在 Vue 中,Object.defineProperty 方法被广泛地应用于组件的实现中,例如: 1. 监听数据变化,通过设置 getter 和 setter 方法,实现数据的响应式更新。 2. 实现 computed 计算属性,通过设置 getter 方法,实现计算属性的缓存和响应式更新。 3. 实现 watch 监听器,通过设置 getter 方法,监听数据的变化并触发回调函数。 以上就是我对你提出的问题的回答。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值