我們繼續this._init() 的初始化相關操作, 接着又會執行如下三個初始化方法:
initInjections(vm)
initState(vm)
initProvide(vm)
5. initInjections(vm): 主要作用是初始化inject, 可以訪問到對應的依賴。
inject 和 provide 這裏需要簡單的提一下, 這是[email protected] 版本添加的一對需要一起使用的API, 它 允許父級組件向它之後的子孫組件提供依賴,讓子孫組件無論嵌套多深都可以訪問到.
- provide : 提供一個對象或是返回一個對象的函數。
- inject : 是一個字符串數組或對象。
這一對API 在 Vue 官網有給出兩條實用提示:
provide 和 inject 主要爲高階插件/組件庫提供用例。 並不推薦直接用於應用程序代碼中。
- 大概是因爲會讓組件數據層級關係變的混亂的緣故, 但在開發組件庫時會很好使。
provide 和 inject 綁定並不是可響應的。 這是刻意爲之的。 然而,如果你傳入一個可以監聽的對象,那麼其對象的屬性還是可響應的。
- 有個小技巧, 這裏可以將根組件data 內定義的屬性提供給子孫組件, 這樣在不借助vuex 的情況下就可以實現簡單的全局狀態管理。
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。 接下來我們來看下這功能究竟怎麼實現的~
雖然inject 和 provide 是成對使用的, 但是二者內部是分開初始化的。 從上面三個初始化方法就能看出, 先初始化inject, 然後初始化props/data狀態相關, 最後初始化provide 。 這樣做的目的是可以在 props/data 中使用 inject 內所注入的內容。
我們首先來看一下初始化inject 時的方法定義:
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
}
... [email protected]後新增設置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鉤子函數, 還是分別用一句話來介紹它們主要乾了什麼事:
- initInjections(vm): 讓子組件inject的項可以訪問到正確的值。
- initState(vm): 將組件定義的狀態掛載到this下。
- initProvide (vm): 初始化父組件提供的provide依賴。
- created: 執行組件的 created 鉤子函數。
初始化階段算是告一段落了, 接下來我們會進入組件的掛載階段。
本章還是以一個問題來結尾:
-
請問methods 內的方法可以使用箭頭函數麼? 會產生怎樣的結果?
解答: -
是不可以使用箭頭函數的, 因爲箭頭函數的this是在定義時就綁定的。 在vue的內部, methods 內每個方法的上下文是當前的vm組件實例, methods[key].bind(vm)。 如果使用箭頭函數,函數的上下文就變成了父級的上下文, 也就是undefined了, 結果就是通過undefined 訪問任何變量都會報錯。