每个Vue应用都是从创建Vue实例开始的,这里我们就以一个简单的例子为基础,慢慢深究Vue的实现细节。
<div id="app">{{ a }}</div>
var vm = new Vue({
el: '#app',
data: { a: 1 }
})
当我们重新设置a属性时(vm.a = 2
),视图上显示的值也会变成2。这么简单的例子大家都知道啦,现在就看看使用Vue构造函数初始化的时候都发生了什么。
打开/src/core/instance/index.js
文件,看到Vue构造函数的定义如下:
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)
}
由此可知首先执行了this._init(options)
代码,_init方法在 src/core/instance/init.js
文件中被添加到了Vue原型上,我们看看该方法做了什么。
const vm: Component = this
// a uid
vm._uid = uid++
首先是定义了vm,它的值就是this,即当前实例。接着定义了一个实例属性_uid
,它是Vue组件的唯一标识,每实例化一个Vue组件就会递增。
接下来是在非生产环境下可以测试性能的一段代码:
let startTag, endTag
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
startTag = `vue-perf-start:${vm._uid}`
endTag = `vue-perf-end:${vm._uid}`
mark(startTag)
}
...
/* 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)
}
省略了中间的代码。这段代码的执行条件是:非生产环境,config.performance为true 和 mark都存在的情况下。官方提供了performance的全局API。mark和measure在core/util/perf.js
文件中,其实就是window.performance.mark和window.performance.measure. 组件初始化的性能追踪就是在代码的开头和结尾分别用mark打上标记,然后通过measure函数对两个mark进行性能计算。
再看看中间代码,也就是被性能追踪的代码:
// a flag to avoid this being observed
vm._isVue = true
// merge 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
)
}
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
initProxy(vm)
} else {
vm._renderProxy = vm
}
// expose real self
vm._self = vm
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')
先是设置了_isVue
实例属性,作为一个标志避免Vue实例被响应系统观测。
接下来是合并选项的处理,我们并没有使用_isComponent
属性,所以上面的代码会走else分支,挂载了实例属性$options
, 该属性的生成通过调用了mergeOptions方法,接下来我们看看mergeOptions方法都干了些什么。
mergeOptions 函数来自于 core/util/options.js
文件, 该函数接受三个参数。先来看一下_init
函数中调用该函数时传递的参数分别是什么。
vm.$options = mergeOptions(resolveConstructorOptions(vm.constructor), options || {}, vm)
后两个参数都好理解,options是我们实例化时传过来的参数
{
el: '#app',
data: { a: 1 }
}
vm就是当前实例。
重点看一下第一个参数,是调用方法生成的resolveConstructorOptions(vm.constructor)
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
}
传的参数是vm.constructor
,在我们例子中就是Vue构造函数,因为我们是直接调用的Vue创建的实例。那什么时候不是Vue构造函数呢,在用Vue.extend()
去创建子类,再用子类构造实例的时候,vm.constructor就是子类而不是Vue构造函数了。例如在官方文档上的例子:
// 创建构造器
var Profile = Vue.extend({
template: '<p>{{firstName}} {{lastName}} aka {{alias}}</p>',
data: function () {
return {
firstName: 'Walter',
lastName: 'White',
alias: 'Heisenberg'
}
}
})
// 创建 Profile 实例,并挂载到一个元素上。
new Profile().$mount('#mount-point')
vm.constructor就是Profile。
再看if语句块,是在Ctor.super
为真的情况下执行,super
是子类才有的属性,所以在我们的例子中是不执行的,直接返回options,即Vue.options, 它的值如下:
Vue.options = {
components: {
KeepAlive
Transition,
TransitionGroup
},
directives:{
model,
show
},
filters: Object.create(null),
_base: Vue
}
不记得options是如何形成的可以看一下Vue源码解析一——骨架梳理。现在三个参数已经搞清楚了,就来看看mergeOptions方法发生了什么吧。
检查组件名是否合法
mergeOptions
方法在core/util/options.js
文件中,我们找到该方法,首先看一下方法上方的注释:
/**
* Merge two option objects into a new one.
* Core utility used in both instantiation and inheritance.
*/
合并两个选项对象为一个新的对象。在实例化和继承中使用的核心实用程序。实例化就是调用_init
方法的时候,继承也就是使用Vue.extend
的时候。现在我们知道了该方法的作用,就来看一下该方法的具体实现吧
if (process.env.NODE_ENV !== 'production') {
checkComponents(child)
}
在非生产环境下,会去校验组件的名字是否合法,checkComponents
函数就是用来干这个的,该函数也在当前文件中,找到该函数:
/**
* Validate component names
*/
function checkComponents (options: Object) {
for (const key in options.components) {
validateComponentName(key)
}
}
一个for in
循环遍历options.components
,以子组件的名字为参数调用validateComponentName
方法,所以该方法才是检测组件名是否合法的具体实现。源码如下:
export function validateComponentName (name: string) {
if (!new RegExp(`^[a-zA-Z][\\-\\.0-9_${unicodeLetters}]*$`).test(name)) {
warn(
'Invalid component name: "' + name + '". Component names ' +
'should conform to valid custom element name in html5 specification.'
)
}
if (isBuiltInTag(name) || config.isReservedTag(name)) {
warn(
'Do not use built-in or reserved HTML elements as component ' +
'id: ' + name
)
}
}
该方法由两个if语句块组成,要想组件名合法,必须满足这两个if条件:
- 正则表达式
/^[a-zA-Z][\\-\\.0-9_${unicodeLetters}]*$/
-
isBuiltInTag(name) || config.isReservedTag(name)
条件不成立
对于条件一就是要使用符合html5规范中的有效自定义元素名称
条件二是使用了两个方法来检测的,isBuiltInTag
方法用来检测是否是内置标签,在shared/util.js
文件中定义
/**
* Check if a tag is a built-in tag.
*/
export const isBuiltInTag = makeMap('slot,component', true)
isBuiltInTag
方法是调用makeMap()
生成的,看一下makeMap
的定义:
/**
* Make a map and return a function for checking if a key
* is in that map.
*/
export function makeMap (
str: string,
expectsLowerCase?: boolean
): (key: string) => true | void {
const map = Object.create(null)
const list: Array<string> = str.split(',')
for (let i = 0; i < list.length; i++) {
map[list[i]] = true
}
return expectsLowerCase
? val => map[val.toLowerCase()]
: val => map[val]
}
该方法最后返回一个函数,函数接收一个参数,如果参数在map中就返回true,否则返回undefined。map是根据调用makeMap方法时传入的参数生成的,按照来处来看,也就是
map = { slot: true, component: true }
由此可知slot
和 component
是作为Vue的内置标签而存在的,我们的组件命名不能使用它们。
还有一个方法config.isReservedTag
在core/config.js
文件中定义,在platforms/web/runtime/index.js
文件中被覆盖
Vue.config.isReservedTag = isReservedTag
isReservedTag
方法在platforms/web/util/element.js
文件中,
export const isReservedTag = (tag: string): ?boolean => {
return isHTMLTag(tag) || isSVG(tag)
}
就是检测是否是规定的html标签和svg标签。到此组件名是否合法的检测就结束了。
if (typeof child === 'function') {
child = child.options
}
这里是一个判断,如果child是一个function,就取它的options静态属性。什么函数具有options属性呢?Vue构造函数和使用Vue.extend()创建的'子类',这就允许我们在进行选项合并的时候,去合并一个 Vue 实例构造者的选项了。
规范化Props
normalizeProps(child, vm)
normalizeInject(child, vm)
normalizeDirectives(child)
这是三个规范化选项的函数调用,分别是针对props
, inject
, directives
。为什么会有规范化选项这一步呢?因为我们在使用选项的时候可以有多种不同的用法,比如props, 既可以是字符串数组也可以是对象:
props: ['test1', 'test2']
props: {
test1: String,
test2: {
type: String,
default: ''
}
}
这方便了我们使用,但是Vue要对选项进行处理,多种形式定然增加了复杂度,所以要处理成一种格式,这就是该函数的作用。
我们分别来看具体是怎么规范化的,首先是函数normalizeProps
:
/**
* Ensure all props option syntax are normalized into the
* Object-based format.
*/
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
最后被规范成对象的形式了。先大体看一下函数的结构:
- 先是判断props是否存在,如果不存在直接返回
- if语句处理
数组
props - else if语句块处理
对象
props - 最后如果既不是数组也不对象,还不是生成环境,就发出类型错误的警告
数组类型的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.')
}
}
使用while循环处理每一项,如果是字符串,先用camelize
函数转了一下该字符串,然后存储在了res中,其值是{ type: null }
。camelize
函数定义在shared/util.js
中,其作用就是把连字符格式的字符串转成驼峰式的。比如:
test-a // testA
如果不是字符串类型就发出警告,所以数组格式的props中元素必须是字符串。
数组格式的规范化我们已经了解了,如果我们传的是
props: ['test-a', 'test2']
规范化之后就变成:
props: {
testA: { type: null },
test2: { type: null }
}
再来看看对象props是如何规范化的:
for (const key in props) {
val = props[key]
name = camelize(key)
res[name] = isPlainObject(val)
? val
: { type: val }
}
我们之前举例说过props是对象的话它的属性值有两种写法,一种属性值直接是类型,还有一种属性值是对象。这里的处理是如果是对象的不做处理,是类型的话就把它作为type的值。所以如果我们传的是:
props: {
test1: String,
test2: {
type: String,
default: ''
}
}
规范化之后变成:
props: {
test1: { type: String },
test2: {
type: String,
default: ''
}
}
这样我们就了解了Vue是如何规范化Props的了
规范化inject
inject选项不常使用,我们先来看看官方文档的介绍
// 父级组件提供 'foo'
var Provider = {
provide: {
foo: 'bar'
},
// ...
}
// 子组件注入 'foo'
var Child = {
inject: ['foo'],
created () {
console.log(this.foo) // => "bar"
}
// ...
}
在子组件中并没有定义foo
属性却可以使用,就是因为使用inject
注入了这个属性,而这个属性的值是来源于父组件。和props一样,inject既可以是数组也可以是对象:
inject: ['foo']
inject: { foo },
inject: {
bar: {
from: 'foo',
default: '--'
}
}
为了方便处理,Vue也把它规范成了一种格式,就是对象:
/**
* Normalize all injections into Object-based format
*/
function normalizeInject (options: Object, vm: ?Component) {
const inject = options.inject
if (!inject) return
const normalized = options.inject = {}
if (Array.isArray(inject)) {
} else if (isPlainObject(inject)) {
} else if (process.env.NODE_ENV !== 'production') {
warn(
`Invalid value for option "inject": expected an Array or an Object, ` +
`but got ${toRawType(inject)}.`,
vm
)
}
}
函数开头首先判断inject属性是否存在,如果没有传就直接返回。
接着是数组类型的处理
for (let i = 0; i < inject.length; i++) {
normalized[inject[i]] = { from: inject[i] }
}
for循环遍历整个数组,将元素的值作为key,{ from: inject[i] }
作为值。所以如果是
inject: ['foo']
规范化之后:
inject: { foo: { from: 'foo' } }
然后是处理对象类型的inject:
for (const key in inject) {
const val = inject[key]
normalized[key] = isPlainObject(val)
? extend({ from: key }, val)
: { from: val }
}
使用for in
循环遍历对象,依然使用原来的key作为key,值的话要处理一下,如果原来的值是对象,就用extend函数把{ from: key }
和val混合一下,否则就用val作为from的值。
所以如果我们传入的值是:
inject: {
foo,
bar: {
from: 'foo',
default: '--'
}
}
处理之后变成:
inject: {
foo: { from: 'foo' },
bar: {
from: 'foo',
default: '--'
}
}
最后,如果传入的既不是数组也不是对象,在非生产环境下就会发出警告。
规范化Directives
/**
* Normalize raw function directives into object format.
*/
function normalizeDirectives (options: Object) {
const dirs = options.directives
if (dirs) {
for (const key in dirs) {
const def = dirs[key]
if (typeof def === 'function') {
dirs[key] = { bind: def, update: def }
}
}
}
}
根据官方文档自定义指令的介绍,我们知道注册指令有函数和对象两种形式:
directives: {
'color-swatch': function (el, binding) {
el.style.backgroundColor = binding.value
},
'color-swatch1': {
bind: function (el, binding) {
el.style.backgroundColor = binding.value
}
}
}
该方法就是要把第一种规范化成对象。
看一下方法体,for in 循环遍历所有指令,如果值是函数类型,则把该值作为bind和update属性的值。所以第一种形式规范化之后就变成:
directives: {
'color-swatch': {
bind: function (el, binding) {
el.style.backgroundColor = binding.value
},
update: function (el, binding) {
el.style.backgroundColor = binding.value
}
}
}
现在我们就了解了三个用于规范化选项的函数的作用了。
规范化选项之后是这样一段代码:
// Apply extends and mixins on the child options,
// but only if it is a raw options object that isn't
// the result of another mergeOptions call.
// Only merged options has the _base property.
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)
}
}
}
当child是原始选项对象即没有_base
属性时,进行extends
和mixins
选项的处理。
如果child.extends存在,就递归调用mergeOptions
函数将parent和child.extends进行合并,并将返回值赋给parent。
如果child.mixins存在,for循环遍历child.mixins,也是递归调用mergeOptions
函数将parent和每一项元素进行合并,并更新parent。
mergeOptions
函数我们还没有看完,先继续往下看,这里造成的影响先不追究。之前所做的处理都是前奏,还没有涉及选项合并,是为选项合并所做的铺垫。接下来我们来看选项合并的处理