Vue本质是上来讲是一个函数,在其经过new关键字构造调用时,会完成一系列初始化过程。经过Vue框架进行开发,基本上是经过向Vue函数中传入不一样的参数选项来完成的。参数选项每每须要加以合并,主要有两种状况:html
一、Vue函数自己拥有一些静态属性,在实例化时开发者会传入同名的属性。
二、在使用继承的方式使用Vue时,须要将父类和子类上同名属性加以合并。
vue
Vue函数定义在 /src/core/instance/index.js中。
ios
function Vue (options) {
if (process.env.NODE_ENV !== 'production' &&
!(this instanceof Vue)
) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
this._init(options)
}
initMixin(Vue)
复制代码
在Vue实例化时会将选项集 options 传入到实例原型上的 _init 方法中加以初始化。 initMixin 函数的做用就是向Vue实例的原型对象上添加 _init 方法, initMixin 函数在 /src/core/instance/init.js 中定义。
在 _init 函数中,会对传入的选项集进行合并处理。
web
// merge options
if (options && options._isComponent) {
initInternalComponent(vm, options)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
复制代码
在开发过程当中基本不会传入 _isComponent 选项,所以在实例化时走 else 分支。经过 mergeOptions 函数来返回合并处理以后的选项并将其赋值给实例的 $options 属性。 mergeOptions 函数接收三个参数,其中第一个参数是将生成实例的构造函数传入 resolveConstructorOptions 函数中处理以后的返回值。
npm
export function resolveConstructorOptions (Ctor: Class<Component>) {
let options = Ctor.options
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
}
复制代码
resolveConstructorOptions 函数的参数为实例的构造函数,在构造函数的没有父类时,简单的返回构造函数的 options 属性。反之,则走 if 分支,合并处理构造函数及其父类的 options 属性,如若构造函数的父类仍存在父类则递归调用该方法,最终返回惟一的 options 属性。在研究实例化合并选项时,为行文方便,将该函数返回的值统一称为选项合并的父选项集合,实例化时传入的选项集合称为子选项集合。
json
在合并选项时,在没有继承关系存在的状况,传入的第一个参数为Vue构造函数上的静态属性 options ,那么这个静态属性到底包含什么呢?为了弄清楚这个问题,首先要搞清楚运行 npm run dev 命令来生成 /dist/vue.js 文件的过程当中发生了什么。
在 package.json 文件中 scripts 对象中有:api
"dev": "rollup -w -c scripts/config.js --environment TARGET:web-full-dev",
复制代码
在使用rollup打包时,依据 scripts/config.js 中的配置,并将 web-full-dev 做为环境变量TARGET的值。
数组
// Runtime+compiler development build (Browser)
'web-full-dev': {
entry: resolve('web/entry-runtime-with-compiler.js'),
dest: resolve('dist/vue.js'),
format: 'umd',
env: 'development',
alias: { he: './entity-decoder' },
banner
},
复制代码
上述文件路径是在 scripts/alias.js 文件中配置过别名的。由此可知,执行 npm run dev 命令时,入口文件为 src/platforms/web/entry-runtime-with-compiler.js ,生成符合 umd 规范的 vue.js 文件。依照该入口文件对Vue函数的引用,按图索骥,逐步找到Vue构造函数所在的文件。以下图所示:
浏览器
Vue构造函数定义在 /src/core/instance/index.js中。在该js文件中,经过各类Mixin向 Vue.prototype 上挂载一些属性和方法。以后在 /src/core/index.js 中,经过 initGlobalAPI 函数向Vue构造函数上添加静态属性和方法。
框架
import Vue from './instance/index'
import { initGlobalAPI } from './global-api/index'
import { isServerRendering } from 'core/util/env'
import { FunctionalRenderContext } from 'core/vdom/create-functional-component'
initGlobalAPI(Vue)
复制代码
在initGlobalAPI 函数中有向Vue构造函数中添加 options 属性的定义。
Vue.options = Object.create(null)
ASSET_TYPES.forEach(type => {
Vue.options[type + 's'] = Object.create(null)
})
// this is used to identify the "base" constructor to extend all plain-object
// components with in Weex's multi-instance scenarios.
Vue.options._base = Vue
extend(Vue.options.components, builtInComponents)
复制代码
通过这段代码处理之后,Vue.options 变成这样:
Vue.options = {
components: {
KeepAlive
},
directives: Object.create(null),
filters: Object.create(null),
_base: Vue
}
复制代码
在 /src/platforms/web/runtime/index.js 中,经过以下代码向 Vue.options 属性上添加平台化指令以及内置组件。
import platformDirectives from './directives/index'
import platformComponents from './components/index'
// install platform runtime directives & components
extend(Vue.options.directives, platformDirectives)
extend(Vue.options.components, platformComponents)
复制代码
最终 Vue.options 属性内容以下所示:
Vue.options = {
components: {
KeepAlive,
Transition,
TransitionGroup
},
directives: {
model,
show
},
filters: Object.create(null),
_base: Vue
}
复制代码
合并选项的函数 mergeOptions 在 /src/core/util/options.js 中定义。
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)
if (!child._base) {
if (child.extends) {
parent = mergeOptions(parent, child.extends, 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
}
复制代码
合并选项时,在非生产环境下首先检测声明的组件名称是否合乎标准:
if (process.env.NODE_ENV !== 'production') {
checkComponents(child)
}
复制代码
checkComponents 函数是 对子选项集合的 components 属性中每一个属性使用 validateComponentName 函数进行命名有效性检测。
function checkComponents (options: Object) {
for (const key in options.components) {
validateComponentName(key)
}
}
复制代码
validateComponentName 函数定义了组件命名的规则:
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
)
}
}
复制代码
由上述代码可知,有效性命名规则有两条:
一、组件名称可使用字母、数字、符号 _、符号 - ,且必须以字母为开头。
二、组件名称不能是Vue内置标签 slot 和 component;不能是 html内置标签;不能使用部分SVG标签。
传入Vue的选项形式每每有多种,这给开发者提供了便利。在Vue内部合并选项时却要把各类形式进行标准化,最终转化成一种形式加以合并。
normalizeProps(child, vm)
normalizeInject(child, vm)
normalizeDirectives(child)
复制代码
上述三条函数调用分别标准化选项 props 、inject 、directives 。
props 选项有两种形式:数组、对象,最终都会转化成对象的形式。
若是props 选项是数组,则数组中的值必须都为字符串。若是字符串拥有连字符则转成驼峰命名的形式。好比:
props: ['propOne', 'prop-two']
复制代码
该props将被规范成:
props: {
propOne:{
type: null
},
propTwo:{
type: null
}
}
复制代码
若是props 选项是对象,其属性有两种形式:字符串、对象。属性名有连字符则转成驼峰命名的形式。若是属性是对象,则不变;若是属性是字符串则转变成对象,属性值变成新对象的 type 属性。好比:
props: {
propOne: Number,
"prop-two": Object,
propThree: {
type: String,
default: ''
}
}
复制代码
该props将被规范成:
props: {
propOne: {
type: Number
},
propTwo: {
type: Object
},
propThree: {
type: String,
default: ''
}
}
复制代码
props对象的属性值为对象时,该对象的属性值有效的有四种:
一、type:基础的类型检查。
二、required: 是否为必须传入的属性。
三、default:默认值。
四、validator:自定义验证函数。
inject 选项有两种形式:数组、对象,最终都会转化成对象的形式。
若是inject 选项是数组,则转化为对象,对象的属性名为数组的值,属性的值为仅拥有 from 属性的对象, from 属性的值为与数组对应的值相同。好比:
inject: ['test']
复制代码
该 inject 将被规范成:
inject: {
test: {
from: 'test'
}
}
复制代码
若是inject 选项是对象,其属性有三种形式:字符串、symbol、对象。若是是对象,则添加属性 from ,其值与属性名相等。若是是字符串或者symbol,则转化为对象,对象拥有属性 from ,其值等于该字符串或symbol。好比:
inject: {
a: 'value1',
b: {
default: 'value2'
}
}
复制代码
该 inject 将被规范成:
inject: {
a: {
from: 'value1'
},
b: {
from: 'b',
default: 'value2'
}
}
复制代码
自定义指令选项 directives 只接受对象类型。通常具体的自定义指令是一个对象。 directives 选项的写法较为统一,那么为何还会有这个规范化的步骤呢?那是由于具体的自定义指令对象的属性通常是各个钩子函数。可是Vue提供了一种简写的形式:在 bind 和 update 时触发相同行为,而不关心其它的钩子时,能够直接定义自定义指令为一个函数,而不是对象。
Vue内部合并 directives 选项时,要将这种函数简写,转化成对象的形式。以下:
directive:{
'color':function (el, binding) {
el.style.backgroundColor = binding.value
})
}
复制代码
该 directive 将被规范成:
directive:{
'color':{
bind:function (el, binding) {
el.style.backgroundColor = binding.value
}),
update: function (el, binding) {
el.style.backgroundColor = binding.value
})
}
}
复制代码
mixins 选项接受一个混入对象的数组。这些混入实例对象能够像正常的实例对象同样包含选项。以下所示:
var mixin = {
created: function () { console.log(1) }
}
var vm = new Vue({
created: function () { console.log(2) },
mixins: [mixin]
})
// => 1
// => 2
复制代码
extends 选项容许声明扩展另外一个组件,能够是一个简单的选项对象或构造函数。以下所示:
var CompA = { ... }
// 在没有调用 `Vue.extend` 时候继承 CompA
var CompB = {
extends: CompA,
...
}
复制代码
Vue内部在处理选项extends或mixins时,会先经过递归调用 mergeOptions 函数,将extends对象或mixins数组中的对象做为子选项集合与父选项集合中合并。这就是选项extends和mixins中的内容与并列的其余选项有冲突时的合并规则的依据。
选项的数量比较多,合并规则也不尽相同。Vue内部采用策略模式来合并选项。各类策略方法在 mergeOptions 函数外实现,环境对象为 strats 对象。
strats 对象是在 /src/core/config.js 文件中的 optionMergeStrategies 对象的基础上,进行一系列策略函数添加而获得的对象。环境对象接受请求,来决定委托哪个策略来处理。这也是用户能够经过全局配置 optionMergeStrategies 来自定义选项合并规则的缘由。
环境对象 strats 上拥有的属性以及属性对应的函数以下图所示:
选项 el、 propsData以及图中没有的选项都采用默认策略函数 defaultStrat 进行合并。
const defaultStrat = function (parentVal: any, childVal: any): any {
return childVal === undefined
? parentVal
: childVal
}
复制代码
默认策略比较简单:若是子选项集合中有相应的选项,则直接使用子选项的值;不然使用父选项的值。
选项 data 与 provide 的策略函数虽然都是 mergeDataOrFn,可是选项 provide 合并时是向 mergeDataOrFn函数中传入三个参数:父选项、子选项、实例。选项 data 的合并分两种状况:经过Vue.extends()处理子组件选项时、正常实例化时。前一种状况没有实例 vm,向 mergeDataOrFn函数传入两个参数:父选项和子选项;后一种状况则跟选项 provide 传入的参数同样。
mergeDataOrFn函数代码以下所示,只有在合并 data 选项,且是经过Vue.extends()处理子组件选项时,才会走 if 分支。处理正常的实例化选项 data 、 provide 时,都是走 else 分支。
export function mergeDataOrFn (parentVal: any,childVal: any,vm?: Component): ?Function {
if (!vm) {
if (!childVal) {
return parentVal
}
if (!parentVal) {
return childVal
}
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 () {
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
}
}
}
}
复制代码
在实例 vm 不存在的状况下,有三种状况:
一、子选项不存在,则返回父选项。
二、父选项不存在,则返回子选项。
三、若是父子选项都存在,则返回函数 mergedDataFn 。
函数 mergedDataFn将分别提取父子选项函数的返回值,将该纯对象传入 mergeData 函数,最终返回 mergeData 函数的返回值。若是父子选项都不存在,则不会走到这个函数中,所以不加以考虑。
为何前面说在 if 分支中的父子选项都为函数呢?由于走该分支,只能是经过Vue.extends()处理子组件 data 选项时。而当一个组件被定义时, data 必须声明为返回一个纯对象的函数,这样能防止多个组件实例共享一个数据对象。定义组件时, data 选项是一个纯对象,在非生产环境下,Vue会有错误警告。
在 else 分支中,返回函数 mergedInstanceDataFn ,在该函数中,若是子选项存在则分别提取父子选项函数的返回值,将该纯对象传入 mergeData 函数;不然,将返回纯对象形式的父选项。
在该场景下 mergeData 函数的做用是将父选项对象中有而子选项对象没有的属性,经过 set 方法将该属性添加到子选项对象上并改为响应式数据属性。
分析完各类状况,发现选项 data 与 provide 策略函数是一个高阶函数,返回值是一个返回合并对象的函数。这是为何呢?这个缘由前面说过,是为了保证各组件实例有惟一的数据副本,防止组件实例共享同一数据对象。
选项 data 或 provide选项合并处理的结果是一个函数,并且该函数在合并阶段并无执行,而是在初始化的时候执行的,这又是为何呢?在 /src/core/instance/init.js 进行初始化时有以下代码:
initInjections(vm)
initState(vm)
initProvide(vm)
复制代码
函数 initState 有以下代码:
if (opts.props) initProps(vm, opts.props)
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) {
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}
复制代码
由上述代码可知: data 与 provide 的初始化是在 inject 与 props 以后进行的。在初始化时执行合并函数的返回函数,可以使用 inject 与 props 的值来初始化 data 与 provide 的值。
生命周期钩子选项使用 mergeHook 函数合并。
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
}
复制代码
Vue官方API文档上说生命周期钩子选项只能是函数类型的,从这段源码中能够看出,开发者能够传入函数数组类型的生命周期选项。由于能够将数组中各函数加以合并,所以传入函数数组实用性不大。
还有一个点比较有意思:若是父选项存在,一定是一个数组。虽然生命周期选项能够是数组,可是开发者通常传入的都是函数,那么为何这里父选项一定是数组呢?
这是由于生命周期父选项存在的状况有两种:Vue.extends()、Mixins。在上面 选项extends、mixins的处理 部分已经说过,处理这两种状况时,会将其中的选项做为子选项递归调用 mergeOptions 函数进行合并。也就说声明周期父选项都是通过 mergeHook 函数处理以后的返回值,因此若是生命周期父选项存在,一定是函数数组。
函数 mergeHook 返回值若是存在,会将返回值传入 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
}
复制代码
生命周期钩子数组按顺序执行,所以先执行父选项中的钩子函数,后执行子选项中的钩子函数。
组件 components ,指令 directives ,过滤器 filters ,被称为资源,由于这些均可以做为第三方应用来提供。
资源选项经过 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
}
}
复制代码
先定义合并后选项为空对象。若是父选项存在,则以父选项为原型,不然没有原型。若是子选项为纯对象,则将子选项上的各属性复制到合并后的选项对象上。
前面说过 Vue.options 属性内容以下所示:
Vue.options = {
components: {
KeepAlive,
Transition,
TransitionGroup
},
directives: {
model,
show
},
filters: Object.create(null),
_base: Vue
}
复制代码
KeepAlive 、 Transition 、 TransitionGroup 为内置组件,model , show 为内置指令,不用注册就能够直接使用。
选项 watch 是一个对象,可是对象的属性却能够是多种形式:字符串、函数、对象以及数组。
// 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
复制代码
由于火狐浏览器 Object 原型对象上拥有watch属性,所以在合并前须要检查选项集合 options 上是否有开发者添加的 watch属性,若是没有,不作合并处理。
若是子选项不存在,则返回以父选项为原型的空对象。
若是父选项不存在,先检查子选项是否为纯对象,再返回子选项。
若是父子选项都存在,则先将父选项各属性复制到合并对象上,而后检查子选项上的各个属性。
在子选项上而不在父选项上的属性,是数组则直接添加到合并对象上。若是不是数组,则填充到新数组中,将该数组添加到合并对象上。
父子选项上都存在的属性,将父选项上该属性变成数组格式,再向数组中添加子选项上的对应属性。
选项 props 、 methods 、 inject 、 computed 采用相同的合并策略。选项 methods 与 computed 传入时只接受对象形式,而选项 props 与 inject 通过前面的标准化以后也是纯对象的形式。
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
复制代码
首先检查子选项是否为纯对象,若是不是纯对象,在非生产环境报错。
若是父选项不存在,则直接返回子选项。
若是父选项存在,先建立一个没有原型的空对象做为合并选项对象,将父选项上的各属性复制到合并选项对象上。若是子选项存在,则将子选项对象上的所有属性复制到合并对象上,所以父子选项上有相同属性则以取子选项上该属性的值。最后返回合并选项对象。
一、el 、 propsData 以及采用默认策略合并的选项:有子选项就选用子选项的值,不然选用父选项的值。
二、选项 data 、 provide :返回一个函数,该函数的返回值是合并以后的对象。以子选项对象为基础,若是存在子选项上没有而父选项上有的属性,则将该属性转变成响应式属性后加入到子选项对象上。
三、生命周期钩子选项:合并成函数数组,父选项排在子选项以前,按顺序执行。
四、资源选项(components、directives、filters):定义一个没有原型的空合并对象,子选项存在,则将子选项上的属性复制到合并对象;父选项存在,则以父选项对象为原型。
五、选项 watch :子选项不存在,则返回以父选项为原型的空对象;父选项不存在,返回子选项;父子选项都存在,则和生命周期合并策略相似,以子选项属性为主,转化成数组形式,父选项也存在该属性,则推入数组中。
六、选项props、methods、inject、computed:将父子选项上的属性添加到一个没有原型的空对象上,父子选项上有相同属性的则取子选项的值。
七、子选项中 extends 、 mixins :将这两项的值做为子选项与父选项合并,合并规则依照上述规则合并,最后再分项与子选项的同名属性按上述规则合并。
在合并选项前,先对选项 inject 、 props 和 directives 进行标准化处理。而后将子选项集合中的extends、mixins做为子选项递归调用合并函数与父选项合并。最后使用策略模式合并各个选项。
如需转载,烦请注明出处:www.cnblogs.com/lidengfeng/…