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