首先我們先從一個面試題入手。
面試官問: “Vue中組件通信的常用方式有哪些?”
我答:
1. props
2. 自定義事件
3. eventbus
4. vuex
5. 還有常見的邊界情況$parent、$children、$root、$refs、provide/inject
6. 此外還有一些非props特性$attrs、$listeners
面試官追問:“那你能分別說說他們的原理嗎?”
我:[一臉懵逼]😳
在介紹provide和inject之前我們先簡單看看其他幾個常用屬性。
如果要看別的屬性原理請移步到Vue組件通信原理剖析(一)事件總線的基石 on和on和on和emit和Vue組件通信原理剖析(二)全局狀態管理Vuex
$parent / $root
解決問題:具有相同父類或者相同根元素的組件
// parant
<child1></child1>
<child2></child2>// child1
this.$parent.$on('foo', handle)// child2
this.$parent.$meit('foo')
$children
解決問題:父組件訪問子組件實現父子通信
// parent
this.$children[0].childMethod = '父組件調用子組件方法的輸出'
注意:$children是不能保證子元素的順序
$attrs/$listeners
$attrs 包含了父作用域中不作為prop被識別且獲取的特性綁定屬性(class/style除外),如果子組件沒聲明prop,則包含除clas、style外的所有屬性,并且在子組件中可以通過v-bind="$attrs"
傳入內部組件
// parent
<child foo="foo"></child>// child
<p>{{ $attrs.foo }}</p>
$listeners
包含了父作用域中的 (不含.native
修飾器的)v-on
事件監聽器。它可以通過v-on="$listeners"
傳入內部組件在創建更高層次的組件時非常有用。
簡單點講它是一個對象,里面包含了作用在這個組件上所有的監聽器(監聽事件),可以通過v-on="$listeners"
將事件監聽指向這個組件內的子元素(包括內部的子組件)。
為了查看方便,我們設置`inheritAttrs: true,后面補充一下inheritAttrs。
// parent
<child @click="onclick"></child>// child
// $listeners會被展開并監聽
<p v-on="$listeners"></p>
$refs
解決問題:父組件訪問子組件實現父子通信,和$children類似
// parent
<child ref="children"></child>mounted() {this.$refs.children.childMethod = '父組件調用子組件的輸出'
}
provide/inject
解決問題:能夠實現祖先和后代之間的傳值
// ancestor
provide() {return {foo: 'foo'}
}// descendent
inject: ['foo']
那么問題來了,這個數據通信是什么樣的機制呢?
我們先來看一個列子
// parent 父類
<template><div class=""><p>我是父類</p><child></child></div>
</template>export default {components: {child: () => import('./child')},provide: {foo: '我是祖先類定義provide'},
}// child 子類
<template><div class=""><p>我是子類</p><p>這是inject獲取的值: {{ childFoo }}</p><grand></grand></div>
</template>
export default {components: {grand: () => import('./grand')},inject: { childFoo: { from: 'foo' } },
}// grand 孫類
<template><div class=""><p>我是孫類</p><p>這是inject獲取的值: {{ grandFoo }}</p></div>
</template>
export default {components: {},inject: { grandFoo: { from: 'foo' } },
}
下面我結合上面的示例和源碼一步一步分析一下:
-
先說說provide是怎么定義參數的,源碼走起
// 初始化Provide的實現 export function initProvide (vm: Component) {const provide = vm.$options.provideif (provide) {vm._provided = typeof provide === 'function'? provide.call(vm): provide} }// vm.$options是怎么來的,是通過mergeOpitions得到的 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); }// 我們在看看mergeOptions的實現 const options = {} let key for (key in parent) {mergeField(key) } for (key in child) {if (!hasOwn(parent, key)) {mergeField(key)} } function mergeField (key) {const strat = strats[key] || defaultStratoptions[key] = strat(parent[key], child[key], vm, key) } return options// 找到strat方法的實現 strats.provide = mergeDataOrFn;export function mergeDataOrFn (parentVal: any,childVal: any,vm?: Component ): ?Function {if (!vm) {// in a Vue.extend merge, both should be functionsif (!childVal) {return parentVal}if (!parentVal) {return childVal}return function mergedDataFn () {return mergeData(typeof childVal === 'function' ? childVal.call(this, this) : childVal,typeof parentVal === 'function' ? parentVal.call(this, this) : parentVal)}} else {return function mergedInstanceDataFn () {// instance mergeconst instanceData = typeof childVal === 'function'? childVal.call(vm, vm): childValconst defaultData = typeof parentVal === 'function'? parentVal.call(vm, vm): parentValif (instanceData) {return mergeData(instanceData, defaultData)} else {return defaultData}}} }
從上面的邏輯可以看出,在組件初始化時,會將
vm.$options.provide
這個函數賦值給provide,并把調用該函數得到的結果賦值給vm._provided
,那么就會得到vm._provided = { foo: "我是祖先類定義provide" }
-
不要停,我們繼續探究一下子孫組件中的inject是怎么實現的,上源碼
// 首先,初始化inject export function initInjections (vm: Component) {const result = resolveInject(vm.$options.inject, vm)if (result) {toggleObserving(false)Object.keys(result).forEach(key => {/* istanbul ignore else */if (process.env.NODE_ENV !== 'production') {defineReactive(vm, key, result[key], () => {warn(`Avoid mutating an injected value directly since the changes will be ` +`overwritten whenever the provided component re-renders. ` +`injection being mutated: "${key}"`,vm)})} else {defineReactive(vm, key, result[key])}})toggleObserving(true)} }// 初始化的inject實際上是resolveInject的結果,下面我們看看resolve都有哪些操作 // 第一步:獲取組件中定義的inject的key值,然后進行遍歷 // 第二步:根據key值獲取對應的在provide中定義的provideKey,就比如上面的根據"childFoo"獲取到"foo" // 第三步:通過source = source.$parent逐級往上循環在_provided中查找對應的provideKey // 第四步:如果找到,將實際的key值作為鍵,source._provided[provideKey]作為值,存為一個對象,當作這個函數的結果 export function resolveInject (inject: any, vm: Component): ?Object {if (inject) {// inject is :any because flow is not smart enough to figure out cachedconst result = Object.create(null)const keys = hasSymbol? Reflect.ownKeys(inject): Object.keys(inject)for (let i = 0; i < keys.length; i++) {const key = keys[i]// #6574 in case the inject object is observed...if (key === '__ob__') continueconst provideKey = inject[key].fromlet source = vmwhile (source) {if (source._provided && hasOwn(source._provided, provideKey)) {result[key] = source._provided[provideKey]break}source = source.$parent}if (!source) {if ('default' in inject[key]) {const provideDefault = inject[key].defaultresult[key] = typeof provideDefault === 'function'? provideDefault.call(vm): provideDefault} else if (process.env.NODE_ENV !== 'production') {warn(`Injection "${key}" not found`, vm)}}}return result} }
說到這里,我們應該知道了provide/inject之間的調用邏輯了吧。最后,我們在用一句話總結一下:
當祖先組件在初始化時,vue首先會通過mergeOptions方法將組件中provide配置項合并vm.$options中,并通過mergeDataOrFn將provide的值放入當前實例的_provided
中,此時當子孫組件在初始化時,也會通過合并的options解析出當前組件所定義的inject,并通過網上逐級遍歷查找的方式,在祖先實例的-provided
中找到對應的value值
至此,關于Vue的組件通信原理就介紹完了,希望能對大家有幫助。
全部文章鏈接
Vue組件通信原理剖析(一)事件總線的基石 on和on和on和emit
Vue組件通信原理剖析(二)全局狀態管理Vuex
Vue組件通信原理剖析(三)provide/inject原理分析
最后喜歡我的小伙伴也可以通過關注公眾號“劍指大前端”,或者掃描下方二維碼聯系到我,進行經驗交流和分享,同時我也會定期分享一些大前端干貨,讓我們的開發從此不迷路。