我們最開始的列子是:
<div id="app">{{ a }}</div>
var vm = new Vue({
el: '#app',
data: { a: 1 }
})
初始化執(zhí)行_init
方法俗或,該方法進(jìn)行到vm.$options = mergeOptions(resolveConstructorOptions(vm.constructor), options || {}, vm)
的時候, 我們通過mergeOptions
方法了解了選項規(guī)范化和選項合并在跳。
在我們的例子中,執(zhí)行mergeOptions
方法的時候父選項是Vue.options
, 子選項就是參數(shù){ el: '#app', data: { a: 1} }
决侈。el
使用默認(rèn)合并策略進(jìn)行合并,data
合并完是一個函數(shù),我們在選項合并中分析過了赖歌⊥髌裕看一下vm.$options
的打印結(jié)果
接著往下看_init
方法中的其他代碼
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
initProxy(vm)
} else {
vm._renderProxy = vm
}
這是一個判斷分支,當(dāng)是非生產(chǎn)環(huán)境時執(zhí)行initProxy
方法庐冯,生產(chǎn)環(huán)境時在實例上添加_renderProxy
屬性孽亲。
再看一下initProxy
方法中是如何處理的,該函數(shù)定義在core/instance/proxy.js
文件中:
initProxy = function initProxy (vm) {
if (hasProxy) {
// determine which proxy handler to use
const options = vm.$options
const handlers = options.render && options.render._withStripped
? getHandler
: hasHandler
vm._renderProxy = new Proxy(vm, handlers)
} else {
vm._renderProxy = vm
}
}
整體也是一個判斷分支展父,不過不論是哪個分支返劲,最后都在實例上添加了_renderProxy
屬性。先看一下hasProxy
的定義, 在當(dāng)前文件中:
const hasProxy =
typeof Proxy !== 'undefined' && isNative(Proxy)
isNative
定義在core/util/env.js
文件中:
export function isNative (Ctor: any): boolean {
return typeof Ctor === 'function' && /native code/.test(Ctor.toString())
}
hasProxy的作用就是判斷宿主環(huán)境是否支持原生的 Proxy栖茉。
如果支持Proxy
篮绿,則對實例進(jìn)行代理,代理對象賦給vm._renderProxy
; 如果不支持吕漂,直接給_renderProxy
屬性賦值vm
亲配。
重點看一下支持的情況:
const options = vm.$options
const handlers = options.render && options.render._withStripped
? getHandler
: hasHandler
vm._renderProxy = new Proxy(vm, handlers)
攔截目標(biāo)是vm
, handles就是攔截行為。如果options.render && options.render._withStripped
條件為真惶凝,handlers的值就是getHandler
吼虎,否則是hasHandler
。_withStripped
屬性只在測試代碼中被設(shè)置為true苍鲜,所以一般是走hasHandler
, 它的定義也在該文件中:
const hasHandler = {
has (target, key) {
// key屬性是否存在在target中
const has = key in target
// 是全局變量或者是以_開頭的字符串并且沒有在$data中思灰,返回true
const isAllowed = allowedGlobals(key) ||
(typeof key === 'string' && key.charAt(0) === '_' && !(key in target.$data))
// target沒有該屬性并且不是全局變量
if (!has && !isAllowed) {
// 以 _ 或 $ 開頭的屬性要使用 `$data.${key}`訪問
if (key in target.$data) warnReservedPrefix(target, key)
// 屬性沒定義
else warnNonPresent(target, key)
}
return has || !isAllowed
}
}
has可以攔截以下操作:
- in操作
- Reflect.has()
- with
這里有兩個警告函數(shù):
- warnReservedPrefix, 為了避免和Vue 內(nèi)置的屬性、API 方法沖突混滔,以 _ 或
data.${key}`訪問
const warnReservedPrefix = (target, key) => {
warn(
`Property "${key}" must be accessed with "$data.${key}" because ` +
'properties starting with "$" or "_" are not proxied in the Vue instance to ' +
'prevent conflicts with Vue internals' +
'See: https://vuejs.org/v2/api/#data',
target
)
}
例如
var vm = new Vue({data: {_a: 1}})
vm._a // undefined
vm.$data._a // 1
- warnNonPresent. 在渲染的時候用到了
${key}
屬性遍坟,但是并沒有在實例上定義
const warnNonPresent = (target, key) => {
warn(
`Property or method "${key}" is not defined on the instance but ` +
'referenced during render. Make sure that this property is reactive, ' +
'either in the data option, or for class-based components, by ' +
'initializing the property. ' +
'See: https://vuejs.org/v2/guide/reactivity.html#Declaring-Reactive-Properties.',
target
)
}
比如:
<div>{{ a }}</div>
var vm = new Vue({data: {_a: 1}})
因為沒有在data中定義a
屬性拳亿,就會報上面那段警告,${key}
就是a
這是為了在開發(fā)階段給我們一個友好的提示愿伴,幫助我們快速定位問題肺魁。再回到_init
函數(shù)中看接下來的代碼
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')
首先在實例對象上添加了_self
屬性隔节,其值就是實例本身鹅经。接著執(zhí)行了一系列的initxxx
方法,我們來一個個看怎诫。
初始化之 initLifecycle
initLifecycle
函數(shù)定義在core/instance/lifecycle.js
文件中
export function initLifecycle (vm: Component) {
const options = vm.$options
// locate first non-abstract parent
// parent指向當(dāng)前實例的父組件
let parent = options.parent
// 如果有父組件且當(dāng)前實例不是抽象的
if (parent && !options.abstract) {
// while循環(huán)查找當(dāng)前實例的第一個非抽象的父組件
while (parent.$options.abstract && parent.$parent) {
parent = parent.$parent
}
// 將vm添加到當(dāng)前第一個非抽象的父實例中
parent.$children.push(vm)
}
// 當(dāng)前實例的$parent屬性指向第一個非抽象的父實例
vm.$parent = parent
// 設(shè)置$root實例屬性瘾晃,有父實例就用父實例的$root,沒有就指向當(dāng)前實例
vm.$root = parent ? parent.$root : vm
vm.$children = []
vm.$refs = {}
vm._watcher = null
vm._inactive = null
vm._directInactive = false
vm._isMounted = false
vm._isDestroyed = false
vm._isBeingDestroyed = false
}
首先定義了常量options幻妓,它指向當(dāng)前實例的$options
屬性蹦误。接著定義了parent,它就是vm.$options.parent
的值,也就是當(dāng)前實例的父組件强胰。接著是一個條件判斷parent && !options.abstract
(如果有父組件且當(dāng)前實例不是抽象的), 什么是抽象的組件呢舱沧?
官方文檔中介紹的keep-alive就是一個抽象組件,其中有這個一句話:<keep-alive> 是一個抽象組件:它自身不會渲染一個 DOM 元素偶洋,也不會出現(xiàn)在父組件鏈中
熟吏。所以抽象組件的特性就是
- 不渲染真實DOM
- 不出現(xiàn)在父組件鏈中
再來看這段代碼:
// parent指向當(dāng)前實例的父組件
let parent = options.parent
// 如果有父組件且當(dāng)前實例不是抽象的
if (parent && !options.abstract) {
// while循環(huán)查找當(dāng)前實例的第一個非抽象的父組件
while (parent.$options.abstract && parent.$parent) {
parent = parent.$parent
}
// 將vm添加到當(dāng)前第一個非抽象的父實例中
parent.$children.push(vm)
}
// 當(dāng)前實例的$parent屬性指向第一個非抽象的父實例
vm.$parent = parent
// 設(shè)置$root實例屬性,有父實例就用父實例的$root玄窝,沒有就指向當(dāng)前實例
vm.$root = parent ? parent.$root : vm
如果if條件為假牵寺,也就是當(dāng)前實例是抽象的,就會跳過if語句塊直接設(shè)置實例屬性$parent
和$root
恩脂,并不會將當(dāng)前實例添加到父組件的$children
中帽氓。
如果條件為真,也就是當(dāng)前實例不是抽象的东亦,就會執(zhí)行if語句塊內(nèi)的代碼。通過while循環(huán)查找第一個非抽象的父組件唬渗,因為抽象組件不能作為父級典阵。找到父級(第一個非抽象的父組件)之后,將當(dāng)前組件實例添加到父級的$children
中镊逝,再接著設(shè)置$parent
和$root
實例屬性壮啊。
在這之后又設(shè)置了一些實例屬性
vm.$children = []
vm.$refs = {}
vm._watcher = null
vm._inactive = null
vm._directInactive = false
vm._isMounted = false
vm._isDestroyed = false
vm._isBeingDestroyed = false
初始化之 initEvents
回到_init
函數(shù)中,initLifecycle
執(zhí)行完之后撑蒜,就是initEvents
, 它定義在core/instance/events.js
文件中
export function initEvents (vm: Component) {
vm._events = Object.create(null)
vm._hasHookEvent = false
// init parent attached events
const listeners = vm.$options._parentListeners
if (listeners) {
updateComponentListeners(vm, listeners)
}
}
在實例上添加_events
和_hasHookEvent
兩個屬性并初始化歹啼。那_events
都包含實例的哪些事件呢?我們通過個例子打印看看
<template lang="html">
<div class="">
<button-counter @on-change-count="changeCount"></button-counter>
</div>
</template>
<script>
export default {
components: {
'button-counter': {
data: function () {
return {
count: 0
}
},
template: '<button v-on:click="addCount">You clicked me {{ count }} times.</button>',
mounted () {
console.log('events', this._events);
},
methods: {
addCount () {
this.count++;
this.$emit('on-change-count')
}
}
}
},
methods: {
changeCount () {
console.log('changeCount');
}
}
}
</script>
看一下在子組件中打印_events
的結(jié)果:
結(jié)果是只有在父組件中使用當(dāng)前組件時綁定在當(dāng)前組件上的事件座菠。
// init parent attached events
const listeners = vm.$options._parentListeners
if (listeners) {
updateComponentListeners(vm, listeners)
}
這段代碼應(yīng)該是初始化父組件添加的事件狸眼,沒看太懂,先略過浴滴。
初始化之 initRender
接下來執(zhí)行的是initRender
函數(shù)拓萌,該函數(shù)存在于core/instance/render.js
文件中,函數(shù)體如下:
export function initRender (vm: Component) {
vm._vnode = null // the root of the child tree
vm._staticTrees = null // v-once cached trees
const options = vm.$options
const parentVnode = vm.$vnode = options._parentVnode // the placeholder node in parent tree
const renderContext = parentVnode && parentVnode.context
vm.$slots = resolveSlots(options._renderChildren, renderContext)
vm.$scopedSlots = emptyObject
// bind the createElement fn to this instance
// so that we get proper render context inside it.
// args order: tag, data, children, normalizationType, alwaysNormalize
// internal version is used by render functions compiled from templates
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
// normalization is always applied for the public version, used in
// user-written render functions.
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
// $attrs & $listeners are exposed for easier HOC creation.
// they need to be reactive so that HOCs using them are always updated
const parentData = parentVnode && parentVnode.data
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, () => {
!isUpdatingChildComponent && warn(`$attrs is readonly.`, vm)
}, true)
defineReactive(vm, '$listeners', options._parentListeners || emptyObject, () => {
!isUpdatingChildComponent && warn(`$listeners is readonly.`, vm)
}, true)
} else {
defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, null, true)
defineReactive(vm, '$listeners', options._parentListeners || emptyObject, null, true)
}
}
這么一大段代碼一眼望去讓人頭大升略,我們一點點來看微王,不懂的就暫時略過,不能丟了主線^o^
首先在Vue實例上添加了兩個屬性:_vnode
品嚣、_staticTrees
vm._vnode = null // the root of the child tree
vm._staticTrees = null // v-once cached trees
它們都被初始化為null, 會在合適的地方進(jìn)行賦值并使用
接下來是這樣一段代碼:
const options = vm.$options
const parentVnode = vm.$vnode = options._parentVnode // the placeholder node in parent tree
const renderContext = parentVnode && parentVnode.context
vm.$slots = resolveSlots(options._renderChildren, renderContext)
vm.$scopedSlots = emptyObject
這段代碼是解析處理slot的炕倘,內(nèi)容比較復(fù)雜,先略過~
// bind the createElement fn to this instance
// so that we get proper render context inside it.
// args order: tag, data, children, normalizationType, alwaysNormalize
// internal version is used by render functions compiled from templates
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
// normalization is always applied for the public version, used in
// user-written render functions.
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
這是添加了兩個實例屬性:_c
和$createElement
, 它們都是對createElement方法的再封裝翰撑。
我們都寫過渲染函數(shù)
render: function (createElement) {
return createElement(
'h',
'test'
)
}
其中createElement也可以替換成this.$createElement
render: function () {
return this.$createElement(
'h',
'test'
)
}
最后是這樣一段代碼:
// $attrs & $listeners are exposed for easier HOC creation.
// they need to be reactive so that HOCs using them are always updated
const parentData = parentVnode && parentVnode.data
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, () => {
!isUpdatingChildComponent && warn(`$attrs is readonly.`, vm)
}, true)
defineReactive(vm, '$listeners', options._parentListeners || emptyObject, () => {
!isUpdatingChildComponent && warn(`$listeners is readonly.`, vm)
}, true)
} else {
defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, null, true)
defineReactive(vm, '$listeners', options._parentListeners || emptyObject, null, true)
}
這里是在Vue實例上添加了兩個屬性$attrs
和 $listeners
, 可以用來創(chuàng)建高階組件罩旋,官方文檔上對這兩個屬性有說明
這兩個對象定義的時候用了defineReactive
函數(shù),該函數(shù)的作用是定義響應(yīng)式屬性瘸恼,具體實現(xiàn)在后面看雙向數(shù)據(jù)綁定的時候再看劣挫。也就是說這兩個屬性是響應(yīng)式的,并且是兩個只讀屬性帐我。
生命周期鉤子
剩余的函數(shù)調(diào)用包括這些:
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')
callHook
就是調(diào)用鉤子函數(shù),在beforeCreate
和created
鉤子函數(shù)調(diào)用的中間芬为,調(diào)用了initInjections
媚朦、initState
、initProvide
三個方法日戈,在initState
函數(shù)里面又調(diào)用了initProps
initMethods
initData
initComputed
initWatch
, 所以在beforeCreate
鉤子被調(diào)用時询张,所有與inject、provide props浙炼、methods份氧、data、computed 以及 watch 相關(guān)的內(nèi)容都不能使用弯屈。在created
鉤子函數(shù)中可以使用蜗帜,但是不能訪問dom,因為這時候還沒掛載呢资厉。
我們看一下callHook
的實現(xiàn)方式, 該函數(shù)存在于lifecycle.js
文件中:
export function callHook (vm: Component, hook: string) {
// #7573 disable dep collection when invoking lifecycle hooks
pushTarget()
const handlers = vm.$options[hook]
const info = `${hook} hook`
if (handlers) {
for (let i = 0, j = handlers.length; i < j; i++) {
invokeWithErrorHandling(handlers[i], vm, null, vm, info)
}
}
if (vm._hasHookEvent) {
vm.$emit('hook:' + hook)
}
popTarget()
}
函數(shù)體的開頭和結(jié)尾分別調(diào)用了pushTarget
和popTarget
, 這是為了在調(diào)用生命周期時禁止收集依賴钮糖。
在看選項合并的時候我們知道生命周期鉤子最終被處理成了數(shù)組,所以handlers
如果有值的話就是一個數(shù)組酌住。
所以下面判斷如果handlers
存在的話循環(huán)調(diào)用每一個函數(shù)
if (handlers) {
for (let i = 0, j = handlers.length; i < j; i++) {
invokeWithErrorHandling(handlers[i], vm, null, vm, info)
}
}
invokeWithErrorHandling
定義在core/util/error.js
文件中店归,里面調(diào)用了handlers[i]
函數(shù)
export function invokeWithErrorHandling (
handler: Function,
context: any,
args: null | any[],
vm: any,
info: string
) {
let res
try {
res = args ? handler.apply(context, args) : handler.call(context)
if (isPromise(res)) {
res.catch(e => handleError(e, vm, info + ` (Promise/async)`))
}
} catch (e) {
handleError(e, vm, info)
}
return res
}
為了捕獲可能出現(xiàn)的錯誤,使用try...catch
包裹了函數(shù)調(diào)用酪我。如果函數(shù)調(diào)用返回Promise消痛,用catch捕獲錯誤。
最后是這樣一段代碼:
if (vm._hasHookEvent) {
vm.$emit('hook:' + hook)
}
這是因為我們的生命周期還可以這樣調(diào)用:
<component
@hook:beforeCreate="handleChildBeforeCreate"
@hook:created="handleChildCreated" />
使用@hook:生命周期名
來監(jiān)聽組件
的生命周期都哭,當(dāng)有這種方式時秩伞,_hasHookEvent
屬性就被設(shè)置為true逞带。
因為initInjections
和initState
都有涉及數(shù)據(jù)雙向綁定的內(nèi)容,所以接下來我們先看數(shù)據(jù)雙向綁定纱新。