VUE源码学习第四篇--new Vue都干了啥(options合并)
一、第一部分:属性设置第一部分比较简单,主要设置vm的两个属性。vm._uid = uid++定义了vm唯一标识_uid属性,每次new Vue都会递增。// a flag to avoid this being observedvm._isVue = true定义vm的_isVue属性为true,这个属性从注释看是为了vm对象避免被observed,大家可以先认为和...
一、第一部分:属性设置
第一部分比较简单,主要设置vm的两个属性。
vm._uid = uid++
定义了vm唯一标识_uid属性,每次new Vue都会递增。
// a flag to avoid this being observed
vm._isVue = true
定义vm的_isVue属性为true,这个属性从注释看是为了vm对象避免被observed,大家可以先认为和数据响应有关,后续再关注。
其实第一部分还有一段代码,
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
startTag = `vue-perf-start:${vm._uid}`
endTag = `vue-perf-end:${vm._uid}`
mark(startTag)
}
和第三部分的这段遥相呼应,主要作用是测试中间代码的执行性能,有兴趣的同学可以了解下window.performance
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
vm._name = formatComponentName(vm, false)
mark(endTag)
measure(`vue ${vm._name} init`, startTag, endTag)
}
好了,接下来就是本章节的重点部分了。
二、第二部分:options合并
第二部分代码是对options的合并。目的是将相关的属性和方法都放到vm.$options中,看看是怎么实现的。
if (options && options._isComponent) {
// optimize internal component instantiation
// since dynamic options merging is pretty slow, and none of the
// internal component options needs special treatment.
initInternalComponent(vm, options)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
当满足第一个条件,即是component组件时调用initInternalComponent方法,由于当前是new Vue对象,不是组件,这部分暂时不表,等组件部分再介绍。
那进入另一个条件分支,调用mergeOptions方法实现options的合并,该方法有三个参数,第一个是resolveConstructorOptions方法返回值(vm.constructor的options),第二是new Vue时传入的值(自定义的options),第三个是vue对象本身。mergeOptions就是通过一系列的合并策略,将Vue的构造函数以及自定义的options进行合并。
1、resolveConstructorOptions
进入resolveConstructorOptions方法
export function resolveConstructorOptions (Ctor: Class<Component>) {
let options = Ctor.options
//如果不是vue的子类(vue.extend),则undefined
if (Ctor.super) {
const superOptions = resolveConstructorOptions(Ctor.super)
const cachedSuperOptions = Ctor.superOptions
if (superOptions !== cachedSuperOptions) {
// super option changed,
// need to resolve new options.
Ctor.superOptions = superOptions
// check if there are any late-modified/attached options (#4976)
const modifiedOptions = resolveModifiedOptions(Ctor)
// update base extend options
if (modifiedOptions) {
extend(Ctor.extendOptions, modifiedOptions)
}
options = Ctor.options = mergeOptions(superOptions, Ctor.extendOptions)
if (options.name) {
options.components[options.name] = Ctor
}
}
}
return options
}
该方法的入参是vm.constructor,即构造函数Vue,如果对原型相关知识不熟悉,可以参考我的文章ES6系列教程第五篇--Class基本知识
执行完第一句,我们来看下Ctor.options(即Vue.options)
在这之前,我们没有定义任何这些属性,那这些怎么来的,我们前面介绍了Vue对象是进过层层封装的。
我们从src/platforms/web/entry-runtime-with-compiler.js,看看对options的配置。
import Vue from './runtime/index'
...
export default Vue
该层封装没有对options设置,暂时不表,继续跟踪runtime/index.js
import Vue from 'core/index'
...
// install platform runtime directives & components
extend(Vue.options.directives, platformDirectives)
extend(Vue.options.components, platformComponents)
....
export default Vue
该层在Vue.options上扩展了两个属性,分别为directives以及components
继续跟踪core/index.js,这个文件我们在上一章节分析过,其中调用了initGlobalAPI方法。
...
//Vue.options初始化
Vue.options = Object.create(null)
ASSET_TYPES.forEach(type => {
Vue.options[type + 's'] = Object.create(null)
})
Vue.options._base = Vue
//扩展构建Vue.options.components
extend(Vue.options.components, builtInComponents)
...
终于找到了源头,对Vue.options属性对象进行了初始化,
export const ASSET_TYPES = [
'component',
'directive',
'filter'
]
Vue中对象的初始化很多使用了Object.create(null)语句,这种方式创建的对象,其原型为null,对象更纯净些。
接下来将_base设置为Vue对象本身,并扩展了compontents对象。
我们来梳理下Vue.options构造过程。
再回到resolveConstructorOptions方法,继续执行,判断Ctor.super,用Vue.extend构造子类时,就会添加一个super属性,由于没有使用extend方法,Ctor.super为undefined,后面我们再做分析。
一句话总结,resolveConstructorOptions返回构造函数的options。
2、mergeOptions
mergeOptions位于core/util/options.js,总览下代码
export function mergeOptions (
parent: Object,
child: Object,
vm?: Component
): Object {
if (process.env.NODE_ENV !== 'production') {
//1、合法性校验检查组件名称的合法性
checkComponents(child)
}
//如果传入的是类型是function,则取其options
if (typeof child === 'function') {
child = child.options
}
//2、格式规整,格式化props
normalizeProps(child, vm)
normalizeInject(child, vm)
normalizeDirectives(child)
//3、extends与mixins处理
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)
}
}
//4、使用不同的策略进行合并
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为resolveConstructorOptions返回的值(Vue的构造函数的options),child是我们new Vue时传入的数组对象(自定义的options),vm是Vue对象本身。方便起见,下面我们就用parent和child来称呼这两种options。
(1)合法性校验
if (process.env.NODE_ENV !== 'production') {
//检查组件名称的合法性
checkComponents(child)
}
这段主要是包含的组件名称的合法性校验, 包括不能与html标签冲突,名称包含字符,数字,连接符,并要以字母开头等。
var vm = new Vue({
el:"#app",
...
components:{
childComponent,
secondComponent
}
})
如果创建对象时传入的参数type为function,则取其options
//如果传入的是类型是function,则取其options
if (typeof child === 'function') {
child = child.options
}
(2)输入规整
接下代码是对下面的child属性格式化,对各种输入进行规整,为何有这步呢?由于child是自定义的,在开发时有多种写法,那么就有必要在运行前做个统一。
//格式化
normalizeProps(child, vm)
normalizeInject(child, vm)
normalizeDirectives(child)
我们以normalizeProps为例,具体看下如何规整。先回顾下props,通过props实现对子组件的数据传递,vue对props的定义有两种实现方式。
//数组模式
props: ['title', 'likes', 'isPublished', 'commentIds', 'author'],
...
//对象模式
props:{
title:{
type:String,
default:"this is zte phone"
},
likes:{
type:String,
default:"this like phone"
}
}
normalizeProps代码:
function normalizeProps (options: Object, vm: ?Component) {
const props = options.props
if (!props) return
const res = {}
let i, val, name
if (Array.isArray(props)) {//对数组的处理
....
} else if (isPlainObject(props)) {//对对象的处理
...
} 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
}
这段代码比较清晰,分别对两种不同模式的props进行处理,注意下用isPlainObject判断是否简单对象,是指用字面量或者new Object创建的对象。处理后结果如下所示,这样两种写法在实现了结构上的统一。
normalizeInject是对Inject的处理,Inject不怎么常用,大家可以参考下inject,处理方式与normalizeProps类似,大家可以自行学习下。
normalizeDirectives是对指令函数简写的处理,如果有不熟悉的可以参考指令章节,我们增加个指令
var vm = new Vue({
el:"#app",
data:{
msg:'tttt'
},
directives:{
color:function (el, binding) {
el.style.backgroundColor = binding.value
}
}
})
处理后结构如下,将方法自动赋值给bind和updata钩子。
(3)extends与minxins处理
接下的一段代码就是对extends和mixins属性的处理。递归调用mergeOptions方法,将入参中的extends与mixins合并到parent上。
//对extends以及mixins属性的处理
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)
}
}
判断是否有extends与mixins属性,并调用mergeOptions进行递归合并,我们先来看下这两个属性的用法。
const mixins1={
created(){
console.log('mixins created');
}
}
const mixins2 = {
created () {
console.log('mixins2 created')
}
}
const extend = {
created () {
console.log('extends created')
}
}
var vm = new Vue({
el:"#app",
mixins: [mixins1, mixins2],
extends: extend,
created () {
console.log('组件 created')
}
})
需要注意的是mixins属性接受的数组,而extends只能接受一个对象。执行这段代码的结果是:
执行的顺序,entends要优于mixins,mixins要优于Vue对象定义。所以created方法要按照这个顺序在options中保存下。
对extends以及mixins对象的遍历合并后,parent的对象最终如下:
大家可能会有疑惑,为何要将extend与mixins设置到parent上呢,这些为了后面的策略合并准备的。
(4)合并策略
到此,child与parent上的数据都已准备完毕,接下来就是对child和parent的上相同的属性的合并,以上面的created为例,Vue对象(child)定义的created要与Vue构造函数(parent)的created如何合并,是替换,还是组合?
var options = {};
var key;
for (key in parent) {
mergeField(key);
}
for (key in child) {
if (!hasOwn(parent, key)) {
mergeField(key);
}
}
function mergeField (key) {
var strat = strats[key] || defaultStrat;
options[key] = strat(parent[key], child[key], vm, key);
}
对每种属性(key)有不同的合并策略strat[key],我们以钩子函数的合并为例。定义钩子函数的合并策略的处理方法为megeHook
var LIFECYCLE_HOOKS = [
'beforeCreate',
'created',
'beforeMount',
'mounted',
'beforeUpdate',
'updated',
'beforeDestroy',
'destroyed',
'activated',
'deactivated',
'errorCaptured'
];
...
LIFECYCLE_HOOKS.forEach(function (hook) {
strats[hook] = mergeHook;
});
我们继续看下megeHook函数定义
function mergeHook (
parentVal,
childVal
) {
return childVal
? parentVal
? parentVal.concat(childVal)
: Array.isArray(childVal)
? childVal
: [childVal]
: parentVal
}
这是个嵌套的三元表达式,判断childVal是否值,如果没有则直接返回parentVal,否则继续;判断parentVal是否有值,如果有值则concat连接,否则继续;判断childVal是否是数组,如果是,直接返回,否则转成array。
上面例子中的created钩子函数,在parent和child上都有,所以需要concat连接。我们看下情况是否如此:
created数组增加了child定义的方法,个数由3个变成了4个。
对上面的疑惑是不是有了解答,extends与mixins中定义的各类属性设置到parent上后,就可以通过统一的过程实现与对象中的定义的属性合并。
对其他属性的合并策略我们就不一一分析代码,总体原则:parent与child两者中只有一个有值,则就直接使用该值,如果两者都有,则视情况:
beforeCreate/created/...:child的属性通过contact连接到parent后面。
watch:child的属性通过contact连接到parent后面。
props/methods/computed:child中属性值的覆盖parent的属性值。
data/provide:child中的属性值覆盖parent的属性值。
component/directive/filter:child中的属性值覆盖parent的属性值。
合并完成后返回options,赋值为vm.$options,至此,第二部分完成。
四、总结
本章节重点介绍了options的合并,合并的目的是将相关的属性和方法都放到vm.$options中,为后续的调用做准备工作。
options的属性有两个来源,一个是Vue的构造函数(parent),通过resolveConstructorOptions获取和构建的;一个是new Vue是传入的(child)。
准备工作完成后,通过各属性不同的合并策略,对parent和child相同属性进行合并。最终生成统一的options。
更多推荐
所有评论(0)