讓咱們繼續this._init()
的初始化之旅,接下來又會執行這樣的三個初始化方法:vue
initInjections(vm)
initState(vm)
initProvide(vm)
複製代碼
5. initInjections(vm): 主要做用是初始化inject
,能夠訪問到對應的依賴。面試
inject
和provide
這裏須要簡單的提一下,這是vue@2.2
版本添加的一對須要一塊兒使用的API
,它容許父級組件向它以後的全部子孫組件提供依賴,讓子孫組件不管嵌套多深均可以訪問到,很cool
有木有~vuex
provide
:提供一個對象或是返回一個對象的函數。inject
:是一個字符串數組或對象。這一對API
在vue
官網有給出兩條食用提示:數組
provide
和inject
主要爲高階插件/組件庫提供用例。並不推薦直接用於應用程序代碼中。bash
provide
和inject
綁定並非可響應的。這是刻意爲之的。然而,若是你傳入了一個可監聽的對象,那麼其對象的屬性仍是可響應的。app
data
內定義的屬性提供給子孫組件,這樣在不借助vuex
的狀況下就能夠實現簡單的全局狀態管理,仍是很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!'
}
}
}
複製代碼
一但觸發handleClick
事件以後,不管嵌套多深的子孫組件只要是使用了inject
注入this.app.info
變量的地方都會被響應,這就完成了簡易的vuex
。更多的示例你們能夠去vue
的官網翻閱,這裏就不碼字了,如今咱們來分析下這麼cool
的功能它到底是怎麼實現的~ide
雖然inject
和provide
是成對使用的,可是兩者在內部是分開初始化的。從上面三個初始化方法就能看出,先初始化inject
,而後初始化props/data
狀態相關,最後初始化provide
。這樣作的目的是能夠在props/data
中使用inject
內所注入的內容。函數
咱們首先來看一下初始化inject
時的方法定義:oop
export function initInjections(vm) {
const result = resolveInject(vm.$options.inject, vm) // 找結果
...
}
複製代碼
vm.$options.inject
爲以前合併後獲得的用戶自定義的inject
,而後使用resolveInject
方法找到咱們想要的結果,咱們看下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後新增設置inject默認參數相關邏輯
}
return result
}
}
複製代碼
首先定義一個result
返回找到的結果。接下來使用雙循環查找,外層的for循環會遍歷inject
的每一項,而後再內層使用while
循環自底向上的查找inject
該項的父級是否有提供對應的依賴。
Ps:
這裏可能有人會有疑問,以前inject
的定義明明是數組,這裏怎麼能夠經過Object.keys
取值?這是由於上一章再作options
合併時,也會對參數進行格式化,如props
的格式,定義爲數組也會被轉爲對象格式,inject
被定義時是這樣的:
定義時:
{
inject: ['app']
}
格式化後:
{
inject: {
app: {
from: 'app'
}
}
}
複製代碼
書接上文,source
就是當前的實例,而source._provided
內保存的就是當前provide
提供的值。首先從當前實例查找,接着將它的父組件實例賦值給source
,在它的父組件查找。找到後使用break
跳出循環,將搜索的結果賦值給result
,接着查找下一個。
Ps:
可能有人又會有疑問,這個時候是先初始化的inject
再初始化的provide
,怎麼訪問父級的provide
了?它根本就沒初始化阿,這個時候咱們就要再思考下了,由於vue
是組件式的,首先就會初始化父組件,而後纔是初始化子組件,因此這個時候是有source._provided
屬性的。
找到了想到的結果以後,咱們補全以前initInjections
的定義:
export function initInjections(vm) {
const result = resolveInject(vm.$options.inject, vm)
if(result) { // 若是有結果
toggleObserving(false) // 刻意爲之不被響應式
Object.keys(result).forEach(key => {
...
defineReactive(vm, key, result[key])
})
toggleObserving(true)
}
}
複製代碼
若是有搜索結果,首先會調用toggleObserving(false)
,具體實現不用理會,只用知道這個方法的做用是設置一個標誌位,將決定defineReactive()
方法是否將它的第三個參數設置爲響應式數據,也就是決定result[key]
這個值是否會被設置爲響應式數據,這裏的參數爲false
,只是在vm
下掛載key
對應普通的值,不過這樣就能夠在當前實例使用this
訪問到inject
內對應的依賴項了,設置完畢以後再調用toggleObserving(true)
,改變標誌位,讓defineReactive()
能夠設置第三個參數爲響應式數據(defineReactive
是響應式原理很重要的方法,這裏瞭解便可),也就是它該有的樣子。以上就是inject
實現的相關原理,一句話來講就是,首先遍歷每一項,而後挨個遍歷每一項父級是否有依賴。
6. initState(vm): 初始化會被使用到的狀態,狀態包括props
,methods
,data
,computed
,watch
五個選項。
首先看下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)
}
}
複製代碼
如今這裏的話只會介紹前面三類狀態的初始化作了什麼,也就是props
,methods
,data
,由於computed
和watch
會涉及到響應式相關的watcher
,這裏先略過。接下來咱們依次有請這三位的初始化方法登場:
6.1 initProps (vm, propsOptions):
this
直接訪問。function initProps(vm, propsOptions) { // 第二個參數爲驗證規則
const propsData = vm.$options.propsData || {} // props具體的值
const props = vm._props = {} // 存放props
const isRoot = !vm.$parent // 是不是根節點
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
內的第二個參數propsOptions
,就是當前實例也就是通訊角色裏的子組件,它所定義的接受參數的規則。子組件的props
規則是可使用數組形式的定義的,不過再通過合併options
以後會被格式化爲對象的形式:
定義時:
{
props: ['name', 'age']
}
格式化後:
{
name: {
type: null
},
age: {
type: null
}
}
複製代碼
因此在定義props
規則時,直接使用對象格式吧,這也是更好的書寫規範。
知道了規則以後,接下來須要知道父組件傳遞給子組件具體的值,它以對象的格式被放在vm.$options.propsData
內,這也是合併options
時獲得的。接下來在實例下定義了一個空對象vm._props
,它的做用是將符合規格的值掛載到它下面。isRoot
的做用是判斷當前組件是不是根組件,若是不是就不將props
的轉爲響應式數據。
接下來遍歷格式化後的props
驗證規則,經過validateProp
方法驗證規則並獲得相應的值,將獲得的值掛載到vm._props
下。這個時候就能夠經過this._props
訪問到props
內定義的值了:
props: ['name'],
methods: {
handleClick() {
console.log(this._props.name)
}
}
複製代碼
不過直接訪問內部的私有變量這種方式並不友好,因此vue
內部作了一層代理,將對this.name
的訪問轉而爲對this._props.name
的訪問。這裏的proxy
須要介紹下,由於以後的data
也會使用到,看下它的定義:
格式化了一下:
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
}
})
}
複製代碼
其實很簡單,只是定義一個對象值的get
方法,讀取時讓其返回另外的一個值,這裏就完成了props
的初始化。
6.2 initMethods (vm, methods):
methods
內的方法掛載到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 的簡寫
warn(`只定義了key而沒有相應的value`)
}
if(props && hasOwn(props, key)) {
warn(`方法名和props的key重名了`)
}
if((key in vm) && isReserved(key)) {
warn(`方法名已經存在並且以_或$開頭`)
}
vm[key] = methods[key] == null
? noop // 空函數
: bind(methods[key], vm) // 至關於methods[key].bind(vm)
}
}
複製代碼
methods
的初始化相較而言就簡單了不少。不過它也有不少邊界狀況,如只定義了key
而沒有方法具體的實現、key
和props
重名了、key
已經存在且命名不規範,以_
或$
開頭,至於爲何不行,咱們第一章的時候有說明了。最後將methods
內的方法掛載到this
下,就完成了methods
的初始化。
6.3 initData (vm):
data
,仍是老套路,掛載到this
下。有個重要的點,之因此data
內的數據是響應式的,是在這裏初始化的,這個你們得有個印象~。function initData (vm: Component) {
let data = vm.$options.data
data = vm._data = typeof data === 'function'
? getData(data, vm) // 經過data.call(vm, vm)獲得返回的對象
: data || {}
if (!isPlainObject(data)) { // 若是不是一個對象格式
data = {}
warn(`data得是一個對象`)
}
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內的方法重名了`)
}
if (props && hasOwn(props, key)) {
warn(`和props內的key重名了`)
} else if (!isReserved(key)) { // key不能以_或$開頭
proxy(vm, `_data`, key)
}
}
observe(data, true)
}
複製代碼
首先經過vm.$options.data
獲得用戶定義的data
,若是是function
格式就執行它,並返回執行以後的結果,不然返回data
或{}
,將結果賦值給vm._data
這個私有屬性。和props
同樣的套路,最後用來作一層代理,若是獲得的結果不是對象格式就是報錯了。
而後遍歷data
內的每一項,不能和methods
以及props
內的key
重名,而後使用proxy
作一層代理。注意最後會執行一個方法observe(data, true)
,它的做用了是遞歸的讓data
內的每一項數據都變成響應式的。
其實不難發現它們仨主要作的事情差很少,首先不要相互之間有重名,而後能夠被this
直接訪問到。
7. initProvide(vm): 主要做用是初始化provide
爲子組件提供依賴。
provide
選項應該是一個對象或是函數,因此對它取值便可,就像取data
內的值相似,看下它的定義:
export function initProvide (vm) {
const provide = vm.$options.provide
if (provide) {
vm._provided = typeof provide === 'function'
? provide.call(vm)
: provide
}
}
複製代碼
首先經過vm.$options.provide
取得用戶定義的provide
選項,若是是一個function
類型就執行一下,獲得返回後的結果,將其賦值給了vm._provided
私有屬性,因此子組件在初始化inject
時就能夠訪問到父組件提供的依賴了;若是不是function
類型就直接返回定義的provide
。
8. callHook(vm, 'created'): 執行用戶定義的created
鉤子函數,有mixin
混入的也一併執行。
終於咱們越過了created
鉤子函數,仍是分別用一句話來介紹它們主要都幹了什麼事:
inject
的項能夠訪問到正確的值this
下。provide
依賴。created
鉤子函數初始化的階段算是告一段落了,接下來咱們會進入組件的掛載階段。按照慣例咱們仍是以一道vue
容易被問道的面試題做爲本章的結束吧~:
面試官微笑而又不失禮貌的問道:
methods
內的方法可使用箭頭函數麼,會形成什麼樣的結果?懟回去:
this
是定義時就綁定的。在vue
的內部,methods
內每一個方法的上下文是當前的vm
組件實例,methods[key].bind(vm)
,而若是使用使用箭頭函數,函數的上下文就變成了父級的上下文,也就是undefined
了,結果就是經過undefined
訪問任何變量都會報錯。順手點個贊或關注唄,找起來也方便~