前几篇文章中我们讲到了resolveConstructorOptions,它的主要功能是解析当前实例构造函数上的options,不太明白的同学们可以看本系列的前几篇文章。在解析完其构造函数上的options之后,需要把构造函数上的options和实例化时传入的options进行合并操作并生成一个新的options。这个合并操作就是今天要讲的mergeOptions。如果大家不想看枯燥的讲解,可以直接点击人人都能懂的Vue源码系列—04—mergeOptions-下,翻到文章最后,查看整个mergeOptions的流程图。
Merge two option objects into a new one.
Core utility used in both instantiation and inheritance.
先来看源码中对mergeOptions方法的注释。mergeOptions的功能是合并两个options对象,并生成一个新的对象。是实例化和继承中使用的核心方法。可见mergeOptions方法的重要性。既然这么重要,那我就带大家一行一行的来解析代码。保证大家看完这篇文章后,能彻底的理解mergeOptions的作用。
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
}
normalizeProps(child, vm)
normalizeInject(child, vm)
normalizeDirectives(child)
const extendsFrom = child.extends
if (extendsFrom) {
parent = mergeOptions(parent, extendsFrom, 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
}
首先看传入的三个参数,parent,child,vm,这三个参数分别代表的是该实例构造函数上的options,实例化时传入的options,vm实例本身。结合Vue作者写的注释,我们明白了,原来mergeoptions方法是要合并构造函数和传入的options这两个对象。
明白了这点之后,接下来往下看
if (process.env.NODE_ENV !== 'production') {
checkComponents(child) // 检查组件名称是否合法
}
这段代码主要是判断当前环境是不是生产环境,如果不是,则调用checkComponents方法来检查组件名称是否是可用名称。我们来看看checkComponents的逻辑。
function checkComponents (options: Object) {
for (const key in options.components) {
validateComponentName(key)
}
}
export function validateComponentName (name: string) {
if (!/^[a-zA-Z][\w-]*$/.test(name)) {
warn(
'Invalid component name: "' + name + '". Component names ' +
'can only contain alphanumeric characters and the hyphen, ' +
'and must start with a letter.'
)
}
if (isBuiltInTag(name) || config.isReservedTag(name)) {
warn(
'Do not use built-in or reserved HTML elements as component ' +
'id: ' + name
)
}
}
如果child的options(实例化传入的options)有components属性。如下面这种情况
const app = new Vue({
el: '#app',
...
components: {
childComponent
}
...
})
那么就调用validateComponentName来验证传入的组件名称是否符合以下特征。
- 包含数字,字母,下划线,连接符,并且以字母开头
- 是否和html标签名称或svg标签名称相同
- 是否和关键字名称相同,如undefined, infinity等
如果满足第一条,并且第2,3条都是不相同的话,那么组件名称可用。
我们再回到mergeOptions源码中
if (typeof child === 'function') {
child = child.options
}
如果child是function类型的话,我们取其options属性作为child。
接下来看这三个方法以normalize开头的方法
normalizeProps(child, vm)
normalizeInject(child, vm)
normalizeDirectives(child)
这三个方法的功能类似,分别是把options中的props,inject,directives属性转换成对象的形式。因为有些传入的时候可能会是数组的形式。如
Vue.component('blog-post', {
props: ['postTitle'],
template: '<h3>{{ postTitle }}</h3>'
})
normalizeProps
我们先来看props处理的逻辑
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]
if (typeof val === 'string') {
name = camelize(val)
res[name] = { type: null }
} else if (process.env.NODE_ENV !== 'production') {
warn('props must be strings when using array syntax.')
}
}
} else if (isPlainObject(props)) {
for (const key in props) {
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
}
首先明确这两个方法里的参数是什么,options传入的是child,即实例化时传入的options。vm是实例。知道了这两个参数是什么,我们继续来研究代码。
const props = options.props
if (!props) return
const res = {}
let i, val, name
上面的代码主要是声明一些变量。res用来存放修改后的props,最后把res赋给新的props。下面的逻辑可以分为两种情况来考虑
props是数组
当props是数组的时候,如下面这种情况
Vue.component('blog-post', {
props: ['postTitle'],
template: '<h3>{{ postTitle }}</h3>'
})
它的处理逻辑是,遍历props数组,把数组的每一项的值作为res对象的key,value值等于{type: null},即把上面例子中的['postTitle']转换成下面这种形式
{
postTitle: { type: null }
}
props是对象
当props是对象时,如下面这种情况
Vue.component('my-component', {
props: {
// 必填的字符串
propC: {
type: String,
required: true
}
}
})
这种情况的处理逻辑是遍历对象,先把对象的key值转换成驼峰的形式。然后再判断对象的值,如果是纯对象(即调用object.prototype.toString方法的结果是[object Object]),则直接把对象的值赋值给res,如果不是,则把{ type: 对象的值}赋给res。最终上面这种形式会转换成
{
propC: {
type: String,
required: true
}
}
如果传入的props不是纯对象也不是数组,且当前环境也不是生产环境,则抛出警告。
warn(
`Invalid value for option "props": expected an Array or an Object, ` +
`but got ${toRawType(props)}.`,
vm
)
最后,把处理过的props重新赋值给options.props。
normalizeInject这个方法的逻辑和normalizeProps类似,主要是处理inject。inject属性如果大家平时不是写库或者插件的话,可能很少接触到,可以先查看inject的使用,inject的传入和props类似。可以传入object,也可以传入array
// array
var Child = {
inject: ['foo'],
created () {
console.log(this.foo) // => "bar"
}
// ...
}
// object
const Child = {
inject: {
foo: {
from: 'bar',
default: 'foo'
}
}
}
由于这个方法和normalizeProps逻辑基本一样,这里也不具体展开讲了。上面的demo最终会被转换成如下形式
// array
{
foo: { from: 'foo'}
}
// object
{
foo: {
from: 'bar',
default: 'foo'
}
}
normalizeDirectives
这个方法主要是处理一些自定义指令,如果不了解自定义指令的同学可以自定义指令。这里的方法处理逻辑主要针对自定义指令中函数简写的情况。如下
Vue.directive('color', function (el, binding) {
el.style.backgroundColor = binding.value
})
normalizeDirectives构造函数会把这个指令传入的参数,最终转换成下面这种形式
color: {
bind: function (el, binding) {
el.style.backgroundColor = binding.value
},
update: function (el, binding) {
el.style.backgroundColor = binding.value
}
}
由于文章篇幅所限,本篇文章先讲解到这里,下篇继续带大家来看mergeOptions的实现。