每個Vue應(yīng)用都是從創(chuàng)建Vue實(shí)例開始的兔院,這里我們就以一個簡單的例子為基礎(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屬性時(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)前實(shí)例。接著定義了一個實(shí)例屬性_uid
蚓让,它是Vue組件的唯一標(biāo)識乾忱,每實(shí)例化一個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
文件中蹄葱,其實(shí)就是window.performance.mark和window.performance.measure. 組件初始化的性能追蹤就是在代碼的開頭和結(jié)尾分別用mark打上標(biāo)記,然后通過measure函數(shù)對兩個mark進(jìn)行性能計算锄列。
再看看中間代碼新蟆,也就是被性能追蹤的代碼:
// 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í)例屬性,作為一個標(biāo)志避免Vue實(shí)例被響應(yīng)系統(tǒng)觀測右蕊。
接下來是合并選項的處理琼稻,我們并沒有使用_isComponent
屬性,所以上面的代碼會走else分支饶囚,掛載了實(shí)例屬性$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í)例化時傳過來的參數(shù)
{
el: '#app',
data: { a: 1 }
}
vm就是當(dāng)前實(shí)例规惰。
重點(diǎn)看一下第一個參數(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)建的實(shí)例揩晴。那什么時候不是Vue構(gòu)造函數(shù)呢勋陪,在用Vue.extend()
去創(chuàng)建子類,再用子類構(gòu)造實(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í)例,并掛載到一個元素上劫映。
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源碼解析一——骨架梳理〕镅啵現(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.
*/
合并兩個選項對象為一個新的對象大渤。在實(shí)例化和繼承中使用的核心實(shí)用程序制妄。實(shí)例化就是調(diào)用_init
方法的時候,繼承也就是使用Vue.extend
的時候”萌現(xiàn)在我們知道了該方法的作用耕捞,就來看一下該方法的具體實(shí)現(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
方法磷斧,所以該方法才是檢測組件名是否合法的具體實(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
)
}
}
該方法由兩個if語句塊組成捷犹,要想組件名合法弛饭,必須滿足這兩個if條件:
- 正則表達(dá)式
/^[a-zA-Z][\\-\\.0-9_${unicodeLetters}]*$/
-
isBuiltInTag(name) || config.isReservedTag(name)
條件不成立
對于條件一就是要使用符合html5規(guī)范中的有效自定義元素名稱
條件二是使用了兩個方法來檢測的,isBuiltInTag
方法用來檢測是否是內(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]
}
該方法最后返回一個函數(shù),函數(shù)接收一個參數(shù)枪孩,如果參數(shù)在map中就返回true憔晒,否則返回undefined藻肄。map是根據(jù)調(diào)用makeMap方法時傳入的參數(shù)生成的,按照來處來看拒担,也就是
map = { slot: true, component: true }
由此可知slot
和 component
是作為Vue的內(nèi)置標(biāo)簽而存在的嘹屯,我們的組件命名不能使用它們。
還有一個方法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標(biāo)簽和svg標(biāo)簽蜕窿。到此組件名是否合法的檢測就結(jié)束了谋逻。
if (typeof child === 'function') {
child = child.options
}
這里是一個判斷,如果child是一個function桐经,就取它的options靜態(tài)屬性毁兆。什么函數(shù)具有options屬性呢?Vue構(gòu)造函數(shù)和使用Vue.extend()創(chuàng)建的'子類'阴挣,這就允許我們在進(jìn)行選項合并的時候气堕,去合并一個 Vue 實(shí)例構(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要對選項進(jìn)行處理,多種形式定然增加了復(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
屬性時余蟹,進(jìn)行extends
和mixins
選項的處理卷胯。
如果child.extends存在,就遞歸調(diào)用mergeOptions
函數(shù)將parent和child.extends進(jìn)行合并威酒,并將返回值賦給parent窑睁。
如果child.mixins存在挺峡,for循環(huán)遍歷child.mixins,也是遞歸調(diào)用mergeOptions
函數(shù)將parent和每一項元素進(jìn)行合并担钮,并更新parent橱赠。
mergeOptions
函數(shù)我們還沒有看完鼻听,先繼續(xù)往下看箭昵,這里造成的影響先不追究。之前所做的處理都是前奏狠轻,還沒有涉及選項合并苏遥,是為選項合并所做的鋪墊饼拍。接下來我們來看選項合并的處理