每個(gè)Vue應(yīng)用都是從創(chuàng)建Vue實(shí)例開始的,這里我們就以一個(gè)簡(jiǎn)單的例子為基礎(chǔ),慢慢深究Vue的實(shí)現(xiàn)細(xì)節(jié)。
<div id="app">{{ a }}</div>
var vm = new Vue({
el: '#app',
data: { a: 1 }
})
當(dāng)我們重新設(shè)置a屬性時(shí)(vm.a = 2),視圖上顯示的值也會(huì)變成2。這么簡(jiǎn)單的例子大家都知道啦,現(xiàn)在就看看使用Vue構(gòu)造函數(shù)初始化的時(shí)候都發(fā)生了什么。
打開/src/core/instance/index.js文件,看到Vue構(gòu)造函數(shù)的定義如下:
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)
}
由此可知首先執(zhí)行了this._init(options)代碼,_init方法在 src/core/instance/init.js文件中被添加到了Vue原型上,我們看看該方法做了什么。
const vm: Component = this
// a uid
vm._uid = uid++
首先是定義了vm,它的值就是this,即當(dāng)前實(shí)例。接著定義了一個(gè)實(shí)例屬性_uid,它是Vue組件的唯一標(biāo)識(shí),每實(shí)例化一個(gè)Vue組件就會(huì)遞增。
接下來(lái)是在非生產(chǎn)環(huán)境下可以測(cè)試性能的一段代碼:
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)
}
省略了中間的代碼。這段代碼的執(zhí)行條件是:非生產(chǎn)環(huán)境,config.performance為true 和 mark都存在的情況下。官方提供了performance的全局API。mark和measure在core/util/perf.js文件中,其實(shí)就是window.performance.mark和window.performance.measure. 組件初始化的性能追蹤就是在代碼的開頭和結(jié)尾分別用mark打上標(biāo)記,然后通過(guò)measure函數(shù)對(duì)兩個(gè)mark進(jìn)行性能計(jì)算。
再看看中間代碼,也就是被性能追蹤的代碼:
// 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')
先是設(shè)置了_isVue實(shí)例屬性,作為一個(gè)標(biāo)志避免Vue實(shí)例被響應(yīng)系統(tǒng)觀測(cè)。
接下來(lái)是合并選項(xiàng)的處理,我們并沒(méi)有使用_isComponent屬性,所以上面的代碼會(huì)走else分支,掛載了實(shí)例屬性$options, 該屬性的生成通過(guò)調(diào)用了mergeOptions方法,接下來(lái)我們看看mergeOptions方法都干了些什么。
mergeOptions 函數(shù)來(lái)自于 core/util/options.js 文件, 該函數(shù)接受三個(gè)參數(shù)。先來(lái)看一下_init函數(shù)中調(diào)用該函數(shù)時(shí)傳遞的參數(shù)分別是什么。
vm.$options = mergeOptions(resolveConstructorOptions(vm.constructor), options || {}, vm)
后兩個(gè)參數(shù)都好理解,options是我們實(shí)例化時(shí)傳過(guò)來(lái)的參數(shù)
{
el: '#app',
data: { a: 1 }
}
vm就是當(dāng)前實(shí)例。
重點(diǎn)看一下第一個(gè)參數(shù),是調(diào)用方法生成的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
}
傳的參數(shù)是vm.constructor,在我們例子中就是Vue構(gòu)造函數(shù),因?yàn)槲覀兪侵苯诱{(diào)用的Vue創(chuàng)建的實(shí)例。那什么時(shí)候不是Vue構(gòu)造函數(shù)呢,在用Vue.extend()去創(chuàng)建子類,再用子類構(gòu)造實(shí)例的時(shí)候,vm.constructor就是子類而不是Vue構(gòu)造函數(shù)了。例如在官方文檔上的例子:
// 創(chuàng)建構(gòu)造器
var Profile = Vue.extend({
template: '<p>{{firstName}} {{lastName}} aka {{alias}}</p>',
data: function () {
return {
firstName: 'Walter',
lastName: 'White',
alias: 'Heisenberg'
}
}
})
// 創(chuàng)建 Profile 實(shí)例,并掛載到一個(gè)元素上。
new Profile().$mount('#mount-point')
vm.constructor就是Profile。
再看if語(yǔ)句塊,是在Ctor.super為真的情況下執(zhí)行,super是子類才有的屬性,所以在我們的例子中是不執(zhí)行的,直接返回options,即Vue.options, 它的值如下:
Vue.options = {
components: {
KeepAlive
Transition,
TransitionGroup
},
directives:{
model,
show
},
filters: Object.create(null),
_base: Vue
}
不記得options是如何形成的可以看一下Vue源碼解析一——骨架梳理?,F(xiàn)在三個(gè)參數(shù)已經(jīng)搞清楚了,就來(lái)看看mergeOptions方法發(fā)生了什么吧。
檢查組件名是否合法
mergeOptions方法在core/util/options.js文件中,我們找到該方法,首先看一下方法上方的注釋:
/**
* Merge two option objects into a new one.
* Core utility used in both instantiation and inheritance.
*/
合并兩個(gè)選項(xiàng)對(duì)象為一個(gè)新的對(duì)象。在實(shí)例化和繼承中使用的核心實(shí)用程序。實(shí)例化就是調(diào)用_init方法的時(shí)候,繼承也就是使用Vue.extend的時(shí)候。現(xiàn)在我們知道了該方法的作用,就來(lái)看一下該方法的具體實(shí)現(xiàn)吧
if (process.env.NODE_ENV !== 'production') {
checkComponents(child)
}
在非生產(chǎn)環(huán)境下,會(huì)去校驗(yàn)組件的名字是否合法,checkComponents函數(shù)就是用來(lái)干這個(gè)的,該函數(shù)也在當(dāng)前文件中,找到該函數(shù):
/**
* Validate component names
*/
function checkComponents (options: Object) {
for (const key in options.components) {
validateComponentName(key)
}
}
一個(gè)for in循環(huán)遍歷options.components,以子組件的名字為參數(shù)調(diào)用validateComponentName方法,所以該方法才是檢測(cè)組件名是否合法的具體實(shí)現(xiàn)。源碼如下:
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
)
}
}
該方法由兩個(gè)if語(yǔ)句塊組成,要想組件名合法,必須滿足這兩個(gè)if條件:
- 正則表達(dá)式
/^[a-zA-Z][\\-\\.0-9_${unicodeLetters}]*$/ -
isBuiltInTag(name) || config.isReservedTag(name)條件不成立
對(duì)于條件一就是要使用符合html5規(guī)范中的有效自定義元素名稱
條件二是使用了兩個(gè)方法來(lái)檢測(cè)的,isBuiltInTag方法用來(lái)檢測(cè)是否是內(nèi)置標(biāo)簽,在shared/util.js文件中定義
/**
* Check if a tag is a built-in tag.
*/
export const isBuiltInTag = makeMap('slot,component', true)
isBuiltInTag方法是調(diào)用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]
}
該方法最后返回一個(gè)函數(shù),函數(shù)接收一個(gè)參數(shù),如果參數(shù)在map中就返回true,否則返回undefined。map是根據(jù)調(diào)用makeMap方法時(shí)傳入的參數(shù)生成的,按照來(lái)處來(lái)看,也就是
map = { slot: true, component: true }
由此可知slot 和 component 是作為Vue的內(nèi)置標(biāo)簽而存在的,我們的組件命名不能使用它們。
還有一個(gè)方法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)
}
就是檢測(cè)是否是規(guī)定的html標(biāo)簽和svg標(biāo)簽。到此組件名是否合法的檢測(cè)就結(jié)束了。
if (typeof child === 'function') {
child = child.options
}
這里是一個(gè)判斷,如果child是一個(gè)function,就取它的options靜態(tài)屬性。什么函數(shù)具有options屬性呢?Vue構(gòu)造函數(shù)和使用Vue.extend()創(chuàng)建的'子類',這就允許我們?cè)谶M(jìn)行選項(xiàng)合并的時(shí)候,去合并一個(gè) Vue 實(shí)例構(gòu)造者的選項(xiàng)了。
規(guī)范化Props
normalizeProps(child, vm)
normalizeInject(child, vm)
normalizeDirectives(child)
這是三個(gè)規(guī)范化選項(xiàng)的函數(shù)調(diào)用,分別是針對(duì)props, inject, directives。為什么會(huì)有規(guī)范化選項(xiàng)這一步呢?因?yàn)槲覀冊(cè)谑褂眠x項(xiàng)的時(shí)候可以有多種不同的用法,比如props, 既可以是字符串?dāng)?shù)組也可以是對(duì)象:
props: ['test1', 'test2']
props: {
test1: String,
test2: {
type: String,
default: ''
}
}
這方便了我們使用,但是Vue要對(duì)選項(xiàng)進(jìn)行處理,多種形式定然增加了復(fù)雜度,所以要處理成一種格式,這就是該函數(shù)的作用。
我們分別來(lái)看具體是怎么規(guī)范化的,首先是函數(shù)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
}
根據(jù)注釋我們知道props最后被規(guī)范成對(duì)象的形式了。先大體看一下函數(shù)的結(jié)構(gòu):
- 先是判斷props是否存在,如果不存在直接返回
- if語(yǔ)句處理
數(shù)組props - else if語(yǔ)句塊處理
對(duì)象props - 最后如果既不是數(shù)組也不對(duì)象,還不是生成環(huán)境,就發(fā)出類型錯(cuò)誤的警告
數(shù)組類型的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循環(huán)處理每一項(xiàng),如果是字符串,先用camelize函數(shù)轉(zhuǎn)了一下該字符串,然后存儲(chǔ)在了res中,其值是{ type: null } 。camelize函數(shù)定義在shared/util.js中,其作用就是把連字符格式的字符串轉(zhuǎn)成駝峰式的。比如:
test-a // testA
如果不是字符串類型就發(fā)出警告,所以數(shù)組格式的props中元素必須是字符串。
數(shù)組格式的規(guī)范化我們已經(jīng)了解了,如果我們傳的是
props: ['test-a', 'test2']
規(guī)范化之后就變成:
props: {
testA: { type: null },
test2: { type: null }
}
再來(lái)看看對(duì)象props是如何規(guī)范化的:
for (const key in props) {
val = props[key]
name = camelize(key)
res[name] = isPlainObject(val)
? val
: { type: val }
}
我們之前舉例說(shuō)過(guò)props是對(duì)象的話它的屬性值有兩種寫法,一種屬性值直接是類型,還有一種屬性值是對(duì)象。這里的處理是如果是對(duì)象的不做處理,是類型的話就把它作為type的值。所以如果我們傳的是:
props: {
test1: String,
test2: {
type: String,
default: ''
}
}
規(guī)范化之后變成:
props: {
test1: { type: String },
test2: {
type: String,
default: ''
}
}
這樣我們就了解了Vue是如何規(guī)范化Props的了
規(guī)范化inject
inject選項(xiàng)不常使用,我們先來(lái)看看官方文檔的介紹
// 父級(jí)組件提供 'foo'
var Provider = {
provide: {
foo: 'bar'
},
// ...
}
// 子組件注入 'foo'
var Child = {
inject: ['foo'],
created () {
console.log(this.foo) // => "bar"
}
// ...
}
在子組件中并沒(méi)有定義foo屬性卻可以使用,就是因?yàn)槭褂?code>inject注入了這個(gè)屬性,而這個(gè)屬性的值是來(lái)源于父組件。和props一樣,inject既可以是數(shù)組也可以是對(duì)象:
inject: ['foo']
inject: { foo },
inject: {
bar: {
from: 'foo',
default: '--'
}
}
為了方便處理,Vue也把它規(guī)范成了一種格式,就是對(duì)象:
/**
* 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
)
}
}
函數(shù)開頭首先判斷inject屬性是否存在,如果沒(méi)有傳就直接返回。
接著是數(shù)組類型的處理
for (let i = 0; i < inject.length; i++) {
normalized[inject[i]] = { from: inject[i] }
}
for循環(huán)遍歷整個(gè)數(shù)組,將元素的值作為key,{ from: inject[i] }作為值。所以如果是
inject: ['foo']
規(guī)范化之后:
inject: { foo: { from: 'foo' } }
然后是處理對(duì)象類型的inject:
for (const key in inject) {
const val = inject[key]
normalized[key] = isPlainObject(val)
? extend({ from: key }, val)
: { from: val }
}
使用for in循環(huán)遍歷對(duì)象,依然使用原來(lái)的key作為key,值的話要處理一下,如果原來(lái)的值是對(duì)象,就用extend函數(shù)把{ from: key }和val混合一下,否則就用val作為from的值。
所以如果我們傳入的值是:
inject: {
foo,
bar: {
from: 'foo',
default: '--'
}
}
處理之后變成:
inject: {
foo: { from: 'foo' },
bar: {
from: 'foo',
default: '--'
}
}
最后,如果傳入的既不是數(shù)組也不是對(duì)象,在非生產(chǎn)環(huán)境下就會(huì)發(fā)出警告。
規(guī)范化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 }
}
}
}
}
根據(jù)官方文檔自定義指令的介紹,我們知道注冊(cè)指令有函數(shù)和對(duì)象兩種形式:
directives: {
'color-swatch': function (el, binding) {
el.style.backgroundColor = binding.value
},
'color-swatch1': {
bind: function (el, binding) {
el.style.backgroundColor = binding.value
}
}
}
該方法就是要把第一種規(guī)范化成對(duì)象。
看一下方法體,for in 循環(huán)遍歷所有指令,如果值是函數(shù)類型,則把該值作為bind和update屬性的值。所以第一種形式規(guī)范化之后就變成:
directives: {
'color-swatch': {
bind: function (el, binding) {
el.style.backgroundColor = binding.value
},
update: function (el, binding) {
el.style.backgroundColor = binding.value
}
}
}
現(xiàn)在我們就了解了三個(gè)用于規(guī)范化選項(xiàng)的函數(shù)的作用了。
規(guī)范化選項(xiàng)之后是這樣一段代碼:
// 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)
}
}
}
當(dāng)child是原始選項(xiàng)對(duì)象即沒(méi)有_base屬性時(shí),進(jìn)行extends和mixins選項(xiàng)的處理。
如果child.extends存在,就遞歸調(diào)用mergeOptions函數(shù)將parent和child.extends進(jìn)行合并,并將返回值賦給parent。
如果child.mixins存在,for循環(huán)遍歷child.mixins,也是遞歸調(diào)用mergeOptions函數(shù)將parent和每一項(xiàng)元素進(jìn)行合并,并更新parent。
mergeOptions函數(shù)我們還沒(méi)有看完,先繼續(xù)往下看,這里造成的影響先不追究。之前所做的處理都是前奏,還沒(méi)有涉及選項(xiàng)合并,是為選項(xiàng)合并所做的鋪墊。接下來(lái)我們來(lái)看選項(xiàng)合并的處理