目錄
0.前言
1. vm.$on
1.1 用法回顧
1.2 內部原理
2. vm.$emit
2.1 用法回顧
2.2 內部原理
3. vm.$off
3.1 用法回顧
3.2 內部原理
4. vm.$once
4.1 用法回顧
4.2 內部原理
0.前言
與事件相關的實例方法有4個,分別是vm.$on
、vm.$emit
、vm.$off
和vm.$once
。它們是在eventsMixin
函數中掛載到Vue
原型上的,代碼如下:
export function eventsMixin (Vue) {Vue.prototype.$on = function (event, fn) {}Vue.prototype.$once = function (event, fn) {}Vue.prototype.$off = function (event, fn) {}Vue.prototype.$emit = function (event) {}
}
當執行eventsMixin
函數后,會向Vue
原型上掛載上述4個實例方法。
接下來,我們就來分析這4個與事件相關的實例方法其內部的原理都是怎樣的。
1. vm.$on
1.1 用法回顧
在介紹方法的內部原理之前,我們先根據官方文檔示例回顧一下它的用法。
vm.$on( event, callback )
-
參數:
{string | Array<string>} event
?(數組只在 2.2.0+ 中支持){Function} callback
-
作用:
監聽當前實例上的自定義事件。事件可以由
vm.$emit
觸發。回調函數會接收所有傳入事件觸發函數的額外參數。 -
示例:
vm.$on('test', function (msg) {console.log(msg) }) vm.$emit('test', 'hi') // => "hi"
1.2 內部原理
在介紹內部原理之前,我們先有一個這樣的概念:$on
和$emit
這兩個方法的內部原理是設計模式中最典型的發布訂閱模式,首先定義一個事件中心,通過$on
訂閱事件,將事件存儲在事件中心里面,然后通過$emit
觸發事件中心里面存儲的訂閱事件。
OK,有了這個概念之后,接下來,我們就先來看看$on
方法的內部原理。該方法的定義位于源碼的src/core/instance/event.js
中,如下:
Vue.prototype.$on = function (event, fn) {const vm: Component = thisif (Array.isArray(event)) {for (let i = 0, l = event.length; i < l; i++) {this.$on(event[i], fn)}} else {(vm._events[event] || (vm._events[event] = [])).push(fn)}return vm
}
$on
方法接收兩個參數,第一個參數是訂閱的事件名,可以是數組,表示訂閱多個事件。第二個參數是回調函數,當觸發所訂閱的事件時會執行該回調函數。
首先,判斷傳入的事件名是否是一個數組,如果是數組,就表示需要一次性訂閱多個事件,就遍歷該數組,將數組中的每一個事件都遞歸調用$on
方法將其作為單個事件訂閱。如下:
if (Array.isArray(event)) {for (let i = 0, l = event.length; i < l; i++) {this.$on(event[i], fn)}
}
如果不是數組,那就當做單個事件名來處理,以該事件名作為key
,先嘗試在當前實例的_events
屬性中獲取其對應的事件列表,如果獲取不到就給其賦空數組為默認值,并將第二個參數回調函數添加進去。如下:
else {(vm._events[event] || (vm._events[event] = [])).push(fn)
}
那么問題來了,當前實例的_events
屬性是干嘛的呢?
還記得我們在介紹生命周期初始化階段的初始化事件initEvents
函數中,在該函數中,首先在當前實例上綁定了_events
屬性并給其賦值為空對象,如下:
export function initEvents (vm: Component) {vm._events = Object.create(null)// ...}
這個_events
屬性就是用來作為當前實例的事件中心,所有綁定在這個實例上的事件都會存儲在事件中心_events
屬性中。
以上,就是$on
方法的內部原理。
2. vm.$emit
2.1 用法回顧
在介紹方法的內部原理之前,我們先根據官方文檔示例回顧一下它的用法。
vm.$emit( eventName, […args] )
- 參數:
{string} eventName
[...args]
- 作用: 觸發當前實例上的事件。附加參數都會傳給監聽器回調。
2.2 內部原理
該方法接收的第一個參數是要觸發的事件名,之后的附加參數都會傳給被觸發事件的回調函數。該方法的定義位于源碼的src/core/instance/event.js
中,如下:
Vue.prototype.$emit = function (event: string): Component {const vm: Component = thislet cbs = vm._events[event]if (cbs) {cbs = cbs.length > 1 ? toArray(cbs) : cbsconst args = toArray(arguments, 1)for (let i = 0, l = cbs.length; i < l; i++) {try {cbs[i].apply(vm, args)} catch (e) {handleError(e, vm, `event handler for "${event}"`)}}}return vm}
}
該方法的邏輯很簡單,就是根據傳入的事件名從當前實例的_events
屬性(即事件中心)中獲取到該事件名所對應的回調函數cbs
,如下:
let cbs = vm._events[event]
然后再獲取傳入的附加參數args
,如下:
const args = toArray(arguments, 1)
由于cbs
是一個數組,所以遍歷該數組,拿到每一個回調函數,執行回調函數并將附加參數args
傳給該回調。如下:
for (let i = 0, l = cbs.length; i < l; i++) {try {cbs[i].apply(vm, args)} catch (e) {handleError(e, vm, `event handler for "${event}"`)}
}
以上,就是$emit
方法的內部原理。
3. vm.$off
3.1 用法回顧
在介紹方法的內部原理之前,我們先根據官方文檔示例回顧一下它的用法。
vm.$off( [event, callback] )
-
參數:
{string | Array<string>} event
?(只在 2.2.2+ 支持數組){Function} [callback]
-
作用:
移除自定義事件監聽器。
- 如果沒有提供參數,則移除所有的事件監聽器;
- 如果只提供了事件,則移除該事件所有的監聽器;
- 如果同時提供了事件與回調,則只移除這個回調的監聽器。
3.2 內部原理
通過用法回顧我們知道,該方法用來移除事件中心里面某個事件的回調函數,根據所傳入參數的不同,作出不同的處理。該方法的定義位于源碼的src/core/instance/event.js
中,如下:
Vue.prototype.$off = function (event, fn) {const vm: Component = this// allif (!arguments.length) {vm._events = Object.create(null)return vm}// array of eventsif (Array.isArray(event)) {for (let i = 0, l = event.length; i < l; i++) {this.$off(event[i], fn)}return vm}// specific eventconst cbs = vm._events[event]if (!cbs) {return vm}if (!fn) {vm._events[event] = nullreturn vm}if (fn) {// specific handlerlet cblet i = cbs.lengthwhile (i--) {cb = cbs[i]if (cb === fn || cb.fn === fn) {cbs.splice(i, 1)break}}}return vm
}
可以看到,在該方法內部就是通過不斷判斷所傳參數的情況進而進行不同的邏輯處理,接下來我們逐行分析。
首先,判斷如果沒有傳入任何參數(即arguments.length
為0),這就是第一種情況:如果沒有提供參數,則移除所有的事件監聽器。我們知道,當前實例上的所有事件都存儲在事件中心_events
屬性中,要想移除所有的事件,那么只需把_events
屬性重新置為空對象即可。如下:
if (!arguments.length) {vm._events = Object.create(null)return vm
}
接著,判斷如果傳入的需要移除的事件名是一個數組,就表示需要一次性移除多個事件,那么我們只需同訂閱多個事件一樣,遍歷該數組,然后將數組中的每一個事件都遞歸調用$off
方法進行移除即可。如下:
if (Array.isArray(event)) {for (let i = 0, l = event.length; i < l; i++) {this.$off(event[i], fn)}return vm
}
接著,獲取到需要移除的事件名在事件中心中對應的回調函數cbs
。如下:
const cbs = vm._events[event]
接著,判斷如果cbs
不存在,那表明在事件中心從來沒有訂閱過該事件,那就談不上移除該事件,直接返回,退出程序即可。如下:
if (!cbs) {return vm
}
接著,如果cbs
存在,但是沒有傳入回調函數fn
,這就是第二種情況:如果只提供了事件,則移除該事件所有的監聽器。這個也不難,我們知道,在事件中心里面,一個事件名對應的回調函數是一個數組,要想移除所有的回調函數我們只需把它對應的數組設置為null
即可。如下:
if (!fn) {vm._events[event] = nullreturn vm
}
接著,如果既傳入了事件名,又傳入了回調函數,cbs
也存在,那這就是第三種情況:如果同時提供了事件與回調,則只移除這個回調的監聽器。那么我們只需遍歷所有回調函數數組cbs
,如果cbs
中某一項與fn
相同,或者某一項的fn
屬性與fn
相同,那么就將其從數組中刪除即可。如下:
if (fn) {// specific handlerlet cblet i = cbs.lengthwhile (i--) {cb = cbs[i]if (cb === fn || cb.fn === fn) {cbs.splice(i, 1)break}}
}
以上,就是$off
方法的內部原理。
4. vm.$once
4.1 用法回顧
在介紹方法的內部原理之前,我們先根據官方文檔示例回顧一下它的用法。
vm.$once( event, callback )
-
參數:
{string} event
{Function} callback
-
作用:
監聽一個自定義事件,但是只觸發一次。一旦觸發之后,監聽器就會被移除。
4.2 內部原理
該方法的作用是先訂閱事件,但是該事件只能觸發一次,也就是說當該事件被觸發后會立即移除。要實現這個功能也不難,我們可以定義一個子函數,用這個子函數來替換原本訂閱事件所對應的回調,也就是說當觸發訂閱事件時,其實執行的是這個子函數,然后再子函數內部先把該訂閱移除,再執行原本的回調,以此來達到只觸發一次的目的。
下面我們就來看下源碼的實現。該方法的定義位于源碼的src/core/instance/event.js
中,如下:
Vue.prototype.$once = function (event, fn) {const vm: Component = thisfunction on () {vm.$off(event, on)fn.apply(vm, arguments)}on.fn = fnvm.$on(event, on)return vm
}
可以看到,在上述代碼中,被監聽的事件是event
,其原本對應的回調是fn
,然后定義了一個子函數on
。
在該函數內部,先通過$on
方法訂閱事件,同時所使用的回調函數并不是原本的fn
而是子函數on
,如下:
vm.$on(event, on)
也就是說,當事件event
被觸發時,會執行子函數on
。
然后在子函數內部先通過$off
方法移除訂閱的事件,這樣確保該事件不會被再次觸發,接著執行原本的回調fn
,如下:
function on () {vm.$off(event, on)fn.apply(vm, arguments)
}
另外,還有一行代碼on.fn = fn
是干什么的呢?
上文我們說了,我們用子函數on
替換了原本的訂閱事件所對應的回調fn
,那么在事件中心_events
屬性中存儲的該事件名就會變成如下這個樣子:
vm._events = {'xxx':[on]
}
但是用戶自己卻不知道傳入的fn
被替換了,當用戶在觸發該事件之前想調用$off
方法移除該事件時:
vm.$off('xxx',fn)
此時就會出現問題,因為在_events
屬性中的事件名xxx
對應的回調函數列表中沒有fn
,那么就會移除失敗。這就讓用戶費解了,用戶明明給xxx
事件傳入的回調函數是fn
,現在反而找不到fn
導致事件移除不了了。
所以,為了解決這一問題,我們需要給on
上綁定一個fn
屬性,屬性值為用戶傳入的回調fn
,這樣在使用$off
移除事件的時候,$off
內部會判斷如果回調函數列表中某一項的fn
屬性與fn
相同時,就可以成功移除事件了。
以上,就是$once
方法的內部原理。