對於父子(含跨級)傳遞數據的通信方式,Vue.js 並沒有提供原生的 API 來支持,而是推薦使用大型數據狀態管理工具 Vuex,但 Vuex 對於小型項目來說用起來真的很麻煩。
在 Vue.js 1.x 中,提供了兩個方法:$dispatch
和 $broadcast
,前者用於向上級派發事件,只要是它的父級(一級或多級以上),都可以在組件內通過 $on
(或 events,2.x 已廢棄)監聽到,后者相反,是由上級向下級廣播事件的。
這兩種方法一旦發出事件后,任何組件都是可以接收到的,就近原則,而且會在第一次接收到后停止冒泡,除非返回 true。
下面我們來自行實現 dispatch 和 broadcast 方法,目標是解決父子組件(含跨級)間的通信問題。
我們要實現的 dispatch 和 broadcast 方法,將具有以下功能:
- 在子組件調用 dispatch 方法,向上級指定的組件實例(最近的)上觸發自定義事件,並傳遞數據,且該上級組件已預先通過
$on
監聽了這個事件; - 相反,在父組件調用 broadcast 方法,向下級指定的組件實例(最近的)上觸發自定義事件,並傳遞數據,且該下級組件已預先通過
$on
監聽了這個事件。
先來看下 emitter.js 的代碼:(https://github.com/ElemeFE/element/blob/dev/src/mixins/emitter.js)
function broadcast(componentName, eventName, params) { this.$children.forEach(child => { const name = child.$options.name; if (name === componentName) { child.$emit.apply(child, [eventName].concat(params)); } else { broadcast.apply(child, [componentName, eventName].concat([params])); } }); } export default { methods: { dispatch(componentName, eventName, params) { let parent = this.$parent || this.$root; let name = parent.$options.name; while (parent && (!name || name !== componentName)) { parent = parent.$parent; if (parent) { name = parent.$options.name; } } if (parent) { parent.$emit.apply(parent, [eventName].concat(params)); } }, broadcast(componentName, eventName, params) { broadcast.call(this, componentName, eventName, params); } } };
因為是用作 mixins 導入,所以在 methods 里定義的 dispatch 和 broadcast 方法會被混合到組件里,自然就可以用 this.dispatch
和 this.broadcast
來使用。
這兩個方法都接收了三個參數,第一個是組件的 name
值,用於向上或向下遞歸遍歷來尋找對應的組件,第二個和第三個就是上文分析的自定義事件名稱和要傳遞的數據。
可以看到,在 dispatch 里,通過 while 語句,不斷向上遍歷更新當前組件(即上下文為當前調用該方法的組件)的父組件實例(變量 parent 即為父組件實例),直到匹配到定義的 componentName
與某個上級組件的 name
選項一致時,結束循環,並在找到的組件實例上,調用 $emit
方法來觸發自定義事件 eventName
。broadcast 方法與之類似,只不過是向下遍歷尋找。
來看一下具體的使用方法。有 A.vue 和 B.vue 兩個組件,其中 B 是 A 的子組件,中間可能跨多級,在 A 中向 B 通信:
在element input中的使用(https://github.com/ElemeFE/element/blob/dev/packages/input/src/input.vue)
<!-- A.vue --> <template> <button @click="handleClick">觸發事件</button> </template> <script> import Emitter from '../mixins/emitter.js'; export default { name: 'componentA', mixins: [ Emitter ], methods: { handleClick () { this.broadcast('componentB', 'on-message', 'Hello Vue.js'); } } } </script> // B.vue export default { name: 'componentB', created () { this.$on('on-message', this.showMessage); }, methods: { showMessage (text) { window.alert(text); } } }
同理,如果是 B 向 A 通信,在 B 中調用 dispatch 方法,在 A 中使用 $on 監聽事件即可。