一、背景
基于Vue父子組件的通信規(guī)則,我發(fā)現(xiàn)我對父子組件通信出現(xiàn)了誤解棍辕。首先父組件通過v-bind綁定參數(shù)到子組件身上,然后在子組件內(nèi)通過“emit”还绘,將相應(yīng)的事件“拋出去”楚昭,然后在父組件內(nèi)通過相應(yīng)的事件來接收處理。
這里的問題在于我把這個事情弄錯了拍顷,我一直以為是在子組件內(nèi)注冊相應(yīng)的方法抚太,然后在父組件內(nèi)調(diào)用,其實不然昔案,其實這是一個發(fā)布訂閱模式尿贫。發(fā)布與訂閱都是基于組件本身的,并不能跨組件調(diào)用踏揣,而是由于我們父子組件通信的時候庆亡,發(fā)布的位置與訂閱的位置剛好在邏輯上隔了一層,導致的理解偏差捞稿。我們在子組件本身上注冊了一個事件又谋,形如@eventName=“handler”的形式,這個相當于是在向子組件身上添加訂閱者娱局,然后我們又在子組件內(nèi)部通過this.$emit(‘eventName’, params)彰亥,“發(fā)射”觸發(fā)這個事件,讓父組件內(nèi)部調(diào)用相應(yīng)的方法來處理铃辖。
舉個例子:
mounted() {
this.$on('eventName', (val) => {
console.log(val)
})
this.$emit('eventName', 'hello world')
}
這串代碼先是在mounted生命周期里注冊了一個事件剩愧,然后通過emit觸發(fā)了這個事件,于是我們運行之后將會在瀏覽器控制臺打印以下結(jié)果:
二娇斩、深入思考??
以下是Vue的原型上綁定的emit兩個方法的源碼仁卷,可以看出大佬們寫代碼就是飄逸,考慮周全犬第。這兩個函數(shù)其實就實現(xiàn)了發(fā)布訂閱锦积,通過this的隱式綁定,把事件中心綁定到相應(yīng)的組件上歉嗓,由他來統(tǒng)一處理相應(yīng)的事件的發(fā)布與訂閱丰介。
Vue.prototype.$on = function (event, fn) {
var vm = this;
if (Array.isArray(event)) {
for (var i = 0, l = event.length; i < l; i++) {
vm.$on(event[i], fn);
}
} else {
(vm._events[event] || (vm._events[event] = [])).push(fn);
// optimize hook:event cost by using a boolean flag marked at registration
// instead of a hash lookup
if (hookRE.test(event)) {
vm._hasHookEvent = true;
}
}
return vm
};
Vue.prototype.$emit = function (event) {
var vm = this;
if (process.env.NODE_ENV !== 'production') {
var lowerCaseEvent = event.toLowerCase();
if (lowerCaseEvent !== event && vm._events[lowerCaseEvent]) {
tip(
"Event \"" + lowerCaseEvent + "\" is emitted in component " +
(formatComponentName(vm)) + " but the handler is registered for \"" + event + "\". " +
"Note that HTML attributes are case-insensitive and you cannot use " +
"v-on to listen to camelCase events when using in-DOM templates. " +
"You should probably use \"" + (hyphenate(event)) + "\" instead of \"" + event + "\"."
);
}
}
var cbs = vm._events[event];
if (cbs) {
cbs = cbs.length > 1 ? toArray(cbs) : cbs;
var args = toArray(arguments, 1);
var info = "event handler for \"" + event + "\"";
for (var i = 0, l = cbs.length; i < l; i++) {
invokeWithErrorHandling(cbs[i], vm, args, vm, info);
}
}
return vm
};
基于這個原理,我們可以實現(xiàn)兩個工具函數(shù),這也是iview里面實現(xiàn)父子組件跨級通信的一種方式哮幢,一個叫broadcast带膀,用于向子組件廣播事件,另一個叫dispatch橙垢,相反的是向父組件傳遞事件垛叨。
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 {
// todo 如果 params 是空數(shù)組,接收到的會是 undefined
broadcast.apply(child, [componentName, eventName].concat([params]))
}
})
}
function 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))
}
}
這樣我們就可以在子組件里通過改變了相應(yīng)的事件而向其特定的父組件發(fā)射觸發(fā)事件柜某,但是必須先在父組件里通過$on注冊相應(yīng)的事件嗽元,以及回調(diào)函數(shù)。
這種方式是特別有效的喂击,可以封裝為一個工具函數(shù)剂癌。vue1.0中也實現(xiàn)了這兩個方法,但有點不一樣翰绊,它是通過是否返回true來判斷是否繼續(xù)“冒泡”佩谷,向上或者向下傳遞事件流。但是這些方法在vue2.0中已經(jīng)廢除了辞做。
除此之外琳要,我們還可以通過事件總線(eventBus)實現(xiàn)兄弟組件通信或其他跨級通信,這個原理就是通過在vue上掛載一個新的vue實例秤茅,然后在各個組件中引用它來發(fā)布和訂閱相應(yīng)的事件來通信稚补。
三、總結(jié)
對于框架的學習不要停留在表面框喳,一個工具怎么使用永遠是最基本的要求课幕,但是明白了工具內(nèi)部的具體邏輯,就能更加靈活的使用它五垮。遇到相應(yīng)的問題最好先自己思索乍惊,多看源碼,Vue內(nèi)部有很多好的代碼放仗,好的機制润绎,像我們使用的組件庫的源碼也可以看看,別人的CSS寫法诞挨,函數(shù)寫法莉撇,為什么要這么寫,都是值得我們思考學習的惶傻。