讓我們繼續(xù)this._init()
的初始化之旅,接下來(lái)又會(huì)執(zhí)行這樣的三個(gè)初始化方法:
initInjections(vm)
initState(vm)
initProvide(vm)
5. initInjections(vm): 主要作用是初始化inject
,可以訪問(wèn)到對(duì)應(yīng)的依賴。
inject
和provide
這里需要簡(jiǎn)單的提一下,這是vue@2.2
版本添加的一對(duì)需要一起使用的API
,它允許父級(jí)組件向它之后的所有子孫組件提供依賴,讓子孫組件無(wú)論嵌套多深都可以訪問(wèn)到,很cool
有木有~
-
provide
:提供一個(gè)對(duì)象或是返回一個(gè)對(duì)象的函數(shù)。 -
inject
:是一個(gè)字符串?dāng)?shù)組或?qū)ο蟆?/li>
這一對(duì)API
在vue
官網(wǎng)有給出兩條食用提示:
provide
和inject
主要為高階插件/組件庫(kù)提供用例。并不推薦直接用于應(yīng)用程序代碼中。
- 大概是因?yàn)闀?huì)讓組件數(shù)據(jù)層級(jí)關(guān)系變的混亂的緣故,但在開發(fā)組件庫(kù)時(shí)會(huì)很好使。
provide
和inject
綁定并不是可響應(yīng)的。這是刻意為之的。然而,如果你傳入了一個(gè)可監(jiān)聽的對(duì)象,那么其對(duì)象的屬性還是可響應(yīng)的。
- 有個(gè)小技巧,這里可以將根組件
data
內(nèi)定義的屬性提供給子孫組件,這樣在不借助vuex
的情況下就可以實(shí)現(xiàn)簡(jiǎn)單的全局狀態(tài)管理,還是很cool
的~
app.vue 根組件
export default {
provide() {
return {
app: this
}
},
data() {
return {
info: 'hello world!'
}
}
}
child.vue 子孫組件
export default {
inject: ['app'],
methods: {
handleClick() {
this.app.info = 'hello vue!'
}
}
}
一但觸發(fā)handleClick
事件之后,無(wú)論嵌套多深的子孫組件只要是使用了inject
注入this.app.info
變量的地方都會(huì)被響應(yīng),這就完成了簡(jiǎn)易的vuex
。更多的示例大家可以去vue
的官網(wǎng)翻閱,這里就不碼字了,現(xiàn)在我們來(lái)分析下這么cool
的功能它究竟是怎么實(shí)現(xiàn)的~
雖然inject
和provide
是成對(duì)使用的,但是二者在內(nèi)部是分開初始化的。從上面三個(gè)初始化方法就能看出,先初始化inject
,然后初始化props/data
狀態(tài)相關(guān),最后初始化provide
。這樣做的目的是可以在props/data
中使用inject
內(nèi)所注入的內(nèi)容。
我們首先來(lái)看一下初始化inject
時(shí)的方法定義:
export function initInjections(vm) {
const result = resolveInject(vm.$options.inject, vm) // 找結(jié)果
...
}
vm.$options.inject
為之前合并后得到的用戶自定義的inject
,然后使用resolveInject
方法找到我們想要的結(jié)果,我們看下resolveInject
方法的定義:
export function resolveInject (inject, vm) {
if (inject) {
const result = Object.create(null)
const keys = Object.keys(inject) //省略Symbol情況
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
const provideKey = inject[key].from
let source = vm
while (source) {
if (source._provided && hasOwn(source._provided, provideKey)) { //hasOwn為是否有
result[key] = source._provided[provideKey]
break
}
source = source.$parent
}
... vue@2.5后新增設(shè)置inject默認(rèn)參數(shù)相關(guān)邏輯
}
return result
}
}
首先定義一個(gè)result
返回找到的結(jié)果。接下來(lái)使用雙循環(huán)查找,外層的for循環(huán)會(huì)遍歷inject
的每一項(xiàng),然后再內(nèi)層使用while
循環(huán)自底向上的查找inject
該項(xiàng)的父級(jí)是否有提供對(duì)應(yīng)的依賴。
Ps:
這里可能有人會(huì)有疑問(wèn),之前inject
的定義明明是數(shù)組,這里怎么可以通過(guò)Object.keys
取值?這是因?yàn)樯弦徽略僮?code>options合并時(shí),也會(huì)對(duì)參數(shù)進(jìn)行格式化,如props
的格式,定義為數(shù)組也會(huì)被轉(zhuǎn)為對(duì)象格式,inject
被定義時(shí)是這樣的:
定義時(shí):
{
inject: ['app']
}
格式化后:
{
inject: {
app: {
from: 'app'
}
}
}
書接上文,source
就是當(dāng)前的實(shí)例,而source._provided
內(nèi)保存的就是當(dāng)前provide
提供的值。首先從當(dāng)前實(shí)例查找,接著將它的父組件實(shí)例賦值給source
,在它的父組件查找。找到后使用break
跳出循環(huán),將搜索的結(jié)果賦值給result
,接著查找下一個(gè)。
Ps:
可能有人又會(huì)有疑問(wèn),這個(gè)時(shí)候是先初始化的inject
再初始化的provide
,怎么訪問(wèn)父級(jí)的provide
了?它根本就沒初始化阿,這個(gè)時(shí)候我們就要再思考下了,因?yàn)?code>vue是組件式的,首先就會(huì)初始化父組件,然后才是初始化子組件,所以這個(gè)時(shí)候是有source._provided
屬性的。
找到了想到的結(jié)果之后,我們補(bǔ)全之前initInjections
的定義:
export function initInjections(vm) {
const result = resolveInject(vm.$options.inject, vm)
if(result) { // 如果有結(jié)果
toggleObserving(false) // 刻意為之不被響應(yīng)式
Object.keys(result).forEach(key => {
...
defineReactive(vm, key, result[key])
})
toggleObserving(true)
}
}
如果有搜索結(jié)果,首先會(huì)調(diào)用toggleObserving(false)
,具體實(shí)現(xiàn)不用理會(huì),只用知道這個(gè)方法的作用是設(shè)置一個(gè)標(biāo)志位,將決定defineReactive()
方法是否將它的第三個(gè)參數(shù)設(shè)置為響應(yīng)式數(shù)據(jù),也就是決定result[key]
這個(gè)值是否會(huì)被設(shè)置為響應(yīng)式數(shù)據(jù),這里的參數(shù)為false
,只是在vm
下掛載key
對(duì)應(yīng)普通的值,不過(guò)這樣就可以在當(dāng)前實(shí)例使用this
訪問(wèn)到inject
內(nèi)對(duì)應(yīng)的依賴項(xiàng)了,設(shè)置完畢之后再調(diào)用toggleObserving(true)
,改變標(biāo)志位,讓defineReactive()
可以設(shè)置第三個(gè)參數(shù)為響應(yīng)式數(shù)據(jù)(defineReactive
是響應(yīng)式原理很重要的方法,這里了解即可),也就是它該有的樣子。以上就是inject
實(shí)現(xiàn)的相關(guān)原理,一句話來(lái)說(shuō)就是,首先遍歷每一項(xiàng),然后挨個(gè)遍歷每一項(xiàng)父級(jí)是否有依賴。
6. initState(vm): 初始化會(huì)被使用到的狀態(tài),狀態(tài)包括props
,methods
,data
,computed
,watch
五個(gè)選項(xiàng)。
首先看下initState(vm)
方法的定義:
export function initState(vm) {
...
const opts = vm.$options
if(opts.props) initProps(vm, opts.props)
if(opts.methods) initMethods(vm, opts.methods)
if(opts.data) initData(vm)
...
if(opts.computed) initComputed(vm, opts.computed)
if(opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
現(xiàn)在這里的話只會(huì)介紹前面三類狀態(tài)的初始化做了什么,也就是props
,methods
,data
,因?yàn)?code>computed和watch
會(huì)涉及到響應(yīng)式相關(guān)的watcher
,這里先略過(guò)。接下來(lái)我們依次有請(qǐng)這三位的初始化方法登場(chǎng):
6.1 initProps (vm, propsOptions):
- 主要作用是檢測(cè)子組件接受的值是否符合規(guī)則,以及讓對(duì)應(yīng)的值可以用
this
直接訪問(wèn)。
function initProps(vm, propsOptions) { // 第二個(gè)參數(shù)為驗(yàn)證規(guī)則
const propsData = vm.$options.propsData || {} // props具體的值
const props = vm._props = {} // 存放props
const isRoot = !vm.$parent // 是否是根節(jié)點(diǎn)
if (!isRoot) {
toggleObserving(false)
}
for (const key in propsOptions) {
const value = validateProp(key, propsOptions, propsData, vm)
defineReactive(props, key, value)
if (!(key in vm)) {
proxy(vm, `_props`, key)
}
}
toggleObserving(true)
}
我們知道props
是作為父組件向子組件通信的重要方式,而initProps
內(nèi)的第二個(gè)參數(shù)propsOptions
,就是當(dāng)前實(shí)例也就是通信角色里的子組件,它所定義的接受參數(shù)的規(guī)則。子組件的props
規(guī)則是可以使用數(shù)組形式的定義的,不過(guò)再經(jīng)過(guò)合并options
之后會(huì)被格式化為對(duì)象的形式:
定義時(shí):
{
props: ['name', 'age']
}
格式化后:
{
name: {
type: null
},
age: {
type: null
}
}
所以在定義props
規(guī)則時(shí),直接使用對(duì)象格式吧,這也是更好的書寫規(guī)范。
知道了規(guī)則之后,接下來(lái)需要知道父組件傳遞給子組件具體的值,它以對(duì)象的格式被放在vm.$options.propsData
內(nèi),這也是合并options
時(shí)得到的。接下來(lái)在實(shí)例下定義了一個(gè)空對(duì)象vm._props
,它的作用是將符合規(guī)格的值掛載到它下面。isRoot
的作用是判斷當(dāng)前組件是否是根組件,如果不是就不將props
的轉(zhuǎn)為響應(yīng)式數(shù)據(jù)。
接下來(lái)遍歷格式化后的props
驗(yàn)證規(guī)則,通過(guò)validateProp
方法驗(yàn)證規(guī)則并得到相應(yīng)的值,將得到的值掛載到vm._props
下。這個(gè)時(shí)候就可以通過(guò)this._props
訪問(wèn)到props
內(nèi)定義的值了:
props: ['name'],
methods: {
handleClick() {
console.log(this._props.name)
}
}
不過(guò)直接訪問(wèn)內(nèi)部的私有變量這種方式并不友好,所以vue
內(nèi)部做了一層代理,將對(duì)this.name
的訪問(wèn)轉(zhuǎn)而為對(duì)this._props.name
的訪問(wèn)。這里的proxy
需要介紹下,因?yàn)橹蟮?code>data也會(huì)使用到,看下它的定義:
格式化了一下:
export function proxy(target, sourceKey, key) {
Object.defineProperty(target, key, {
enumerable: true,
configurable: true,
get: function () {
return this[sourceKey][key]
},
set: function () {
this[sourceKey][key] = val
}
})
}
其實(shí)很簡(jiǎn)單,只是定義一個(gè)對(duì)象值的get
方法,讀取時(shí)讓其返回另外的一個(gè)值,這里就完成了props
的初始化。
6.2 initMethods (vm, methods):
- 主要作用是將
methods
內(nèi)的方法掛載到this
下。
function initMethods(vm, methods) {
const props = vm.$options.props
for(const key in methods) {
if(methods[key] == null) { // methods[key] === null || methods[key] === undefined 的簡(jiǎn)寫
warn(`只定義了key而沒有相應(yīng)的value`)
}
if(props && hasOwn(props, key)) {
warn(`方法名和props的key重名了`)
}
if((key in vm) && isReserved(key)) {
warn(`方法名已經(jīng)存在而且以_或$開頭`)
}
vm[key] = methods[key] == null
? noop // 空函數(shù)
: bind(methods[key], vm) // 相當(dāng)于methods[key].bind(vm)
}
}
methods
的初始化相較而言就簡(jiǎn)單了很多。不過(guò)它也有很多邊界情況,如只定義了key
而沒有方法具體的實(shí)現(xiàn)、key
和props
重名了、key
已經(jīng)存在且命名不規(guī)范,以_
或$
開頭,至于為什么不行,我們第一章的時(shí)候有說(shuō)明了。最后將methods
內(nèi)的方法掛載到this
下,就完成了methods
的初始化。
6.3 initData (vm):
- 主要作用是初始化
data
,還是老套路,掛載到this
下。有個(gè)重要的點(diǎn),之所以data
內(nèi)的數(shù)據(jù)是響應(yīng)式的,是在這里初始化的,這個(gè)大家得有個(gè)印象~。
function initData (vm: Component) {
let data = vm.$options.data
data = vm._data = typeof data === 'function'
? getData(data, vm) // 通過(guò)data.call(vm, vm)得到返回的對(duì)象
: data || {}
if (!isPlainObject(data)) { // 如果不是一個(gè)對(duì)象格式
data = {}
warn(`data得是一個(gè)對(duì)象`)
}
const keys = Object.keys(data)
const props = vm.$options.props // 得到props
const methods = vm.$options.methods // 得到methods
let i = keys.length
while (i--) {
const key = keys[i]
if (methods && hasOwn(methods, key)) {
warn(`和methods內(nèi)的方法重名了`)
}
if (props && hasOwn(props, key)) {
warn(`和props內(nèi)的key重名了`)
} else if (!isReserved(key)) { // key不能以_或$開頭
proxy(vm, `_data`, key)
}
}
observe(data, true)
}
首先通過(guò)vm.$options.data
得到用戶定義的data
,如果是function
格式就執(zhí)行它,并返回執(zhí)行之后的結(jié)果,否則返回data
或{}
,將結(jié)果賦值給vm._data
這個(gè)私有屬性。和props
一樣的套路,最后用來(lái)做一層代理,如果得到的結(jié)果不是對(duì)象格式就是報(bào)錯(cuò)了。
然后遍歷data
內(nèi)的每一項(xiàng),不能和methods
以及props
內(nèi)的key
重名,然后使用proxy
做一層代理。注意最后會(huì)執(zhí)行一個(gè)方法observe(data, true)
,它的作用了是遞歸的讓data
內(nèi)的每一項(xiàng)數(shù)據(jù)都變成響應(yīng)式的。
其實(shí)不難發(fā)現(xiàn)它們仨主要做的事情差不多,首先不要相互之間有重名,然后可以被this
直接訪問(wèn)到。
7. initProvide(vm): 主要作用是初始化provide
為子組件提供依賴。
provide
選項(xiàng)應(yīng)該是一個(gè)對(duì)象或是函數(shù),所以對(duì)它取值即可,就像取data
內(nèi)的值類似,看下它的定義:
export function initProvide (vm) {
const provide = vm.$options.provide
if (provide) {
vm._provided = typeof provide === 'function'
? provide.call(vm)
: provide
}
}
首先通過(guò)vm.$options.provide
取得用戶定義的provide
選項(xiàng),如果是一個(gè)function
類型就執(zhí)行一下,得到返回后的結(jié)果,將其賦值給了vm._provided
私有屬性,所以子組件在初始化inject
時(shí)就可以訪問(wèn)到父組件提供的依賴了;如果不是function
類型就直接返回定義的provide
。
8. callHook(vm, 'created'): 執(zhí)行用戶定義的created
鉤子函數(shù),有mixin
混入的也一并執(zhí)行。
終于我們?cè)竭^(guò)了created
鉤子函數(shù),還是分別用一句話來(lái)介紹它們主要都干了什么事:
- initInjections(vm):讓子組件
inject
的項(xiàng)可以訪問(wèn)到正確的值 - initState(vm):將組件定義的狀態(tài)掛載到
this
下。 - initProvide(vm):初始化父組件提供的
provide
依賴。 - created:執(zhí)行組件的
created
鉤子函數(shù)
初始化的階段算是告一段落了,接下來(lái)我們會(huì)進(jìn)入組件的掛載階段。按照慣例我們還是以一道vue
容易被問(wèn)道的面試題作為本章的結(jié)束吧~:
面試官微笑而又不失禮貌的問(wèn)道:
- 請(qǐng)問(wèn)
methods
內(nèi)的方法可以使用箭頭函數(shù)么,會(huì)造成什么樣的結(jié)果?
懟回去:
- 是不可以使用箭頭函數(shù)的,因?yàn)榧^函數(shù)的
this
是定義時(shí)就綁定的。在vue
的內(nèi)部,methods
內(nèi)每個(gè)方法的上下文是當(dāng)前的vm
組件實(shí)例,methods[key].bind(vm)
,而如果使用使用箭頭函數(shù),函數(shù)的上下文就變成了父級(jí)的上下文,也就是undefined
了,結(jié)果就是通過(guò)undefined
訪問(wèn)任何變量都會(huì)報(bào)錯(cuò)。
手點(diǎn)個(gè)贊或關(guān)注唄,找起來(lái)也方便~
分享一個(gè)筆者自己寫的組件庫(kù),哪天可能會(huì)用的上了 ~ ↓