Vue 組件事件觸發(fā)和監(jiān)聽(tīng)實(shí)現(xiàn)源碼解析
正文
通常我們?cè)谑褂?code>Vue的時(shí)候,會(huì)使用$emit和$on來(lái)觸發(fā)和監(jiān)聽(tīng)事件,但是有沒(méi)有思考是如何實(shí)現(xiàn)的呢?
今天帶來(lái)的是一個(gè)微型的事件觸發(fā)的庫(kù),借它們的源碼來(lái)簡(jiǎn)單初步了解Vue的事件觸發(fā)和監(jiān)聽(tīng)的實(shí)現(xiàn)。
mitt使用TypeScript編寫(xiě),tiny-emitter使用原生ES5編寫(xiě),兩者對(duì)比tiny-emitter功能稍微豐富一寫(xiě),所以直接看tiny-emitter就好了。
Vue 的事件觸發(fā)和監(jiān)聽(tīng)
我沒(méi)有標(biāo)題黨,先來(lái)看一下Vue的組件事件怎么使用的:
// 父組件
<template>
<div>
<child @my-event="handleMyEvent"></child>
</div>
</template>
<script>
import Child from './Child.vue'
export default {
components: {
Child
},
methods: {
handleMyEvent (event) {
console.log(event)
}
}
}
</script>
// 子組件
<template>
<div>
<button @click="handleClick">click</button>
</div>
</template>
<script>
export default {
methods: {
handleClick () {
this.$emit('my-event', { a: 1 })
}
}
}
</script>
這個(gè)代碼大家都熟,簡(jiǎn)化之后其實(shí)可以理解成這樣的:
// 父組件
import Child from './Child.js'
const child = new Child()
child.$on('my-event', (event) => {
console.log(event)
});
// 子組件
export default class Child extends Emitter {
constructor () {
super();
}
handleClick () {
this.$emit('my-event', { a: 1 })
}
}
這里可以看到子組件是繼承自Emitter,Emitter是一個(gè)事件觸發(fā)和監(jiān)聽(tīng)的類(lèi),這個(gè)類(lèi)的實(shí)現(xiàn)就是我們今天要學(xué)習(xí)的。
先來(lái)學(xué)習(xí)tiny-emitter的實(shí)現(xiàn),最后把Emitter這個(gè)類(lèi)完成。
源碼分析
tiny-emitter的源碼很簡(jiǎn)單,加上注釋和換行也就68行,先一睹為快:
function E () {
// Keep this empty so it's easier to inherit from
// (via https://github.com/lipsmack from https://github.com/scottcorgan/tiny-emitter/issues/3)
}
E.prototype = {
on: function (name, callback, ctx) {
var e = this.e || (this.e = {});
(e[name] || (e[name] = [])).push({
fn: callback,
ctx: ctx
});
return this;
},
once: function (name, callback, ctx) {
var self = this;
function listener () {
self.off(name, listener);
callback.apply(ctx, arguments);
};
listener._ = callback
return this.on(name, listener, ctx);
},
emit: function (name) {
var data = [].slice.call(arguments, 1);
var evtArr = ((this.e || (this.e = {}))[name] || []).slice();
var i = 0;
var len = evtArr.length;
for (i; i < len; i++) {
evtArr[i].fn.apply(evtArr[i].ctx, data);
}
return this;
},
off: function (name, callback) {
var e = this.e || (this.e = {});
var evts = e[name];
var liveEvents = [];
if (evts && callback) {
for (var i = 0, len = evts.length; i < len; i++) {
if (evts[i].fn !== callback && evts[i].fn._ !== callback)
liveEvents.push(evts[i]);
}
}
// Remove event from queue to prevent memory leak
// Suggested by https://github.com/lazd
// Ref: https://github.com/scottcorgan/tiny-emitter/commit/c6ebfaa9bc973b33d110a84a307742b7cf94c953#commitcomment-5024910
(liveEvents.length)
? e[name] = liveEvents
: delete e[name];
return this;
}
};
module.exports = E;
module.exports.TinyEmitter = E;
一共只有4個(gè)方法,on、once、emit、off,下面一個(gè)一個(gè)開(kāi)始分析。
on
on方法是用來(lái)監(jiān)聽(tīng)事件的,代碼如下:
function on(name, callback, ctx) {
var e = this.e || (this.e = {});
(e[name] || (e[name] = [])).push({
fn: callback,
ctx: ctx
});
return this;
}
它接收三個(gè)參數(shù):
name:事件名稱(chēng)callback:事件觸發(fā)時(shí)的回調(diào)函數(shù)ctx:回調(diào)函數(shù)的上下文
它會(huì)判斷this.e是否存在,如果不存在就創(chuàng)建一個(gè)空對(duì)象;
this.e是用來(lái)存儲(chǔ)事件的,this指向就不多講了吧。
然后把name作為key,callback和ctx作為value,存到this.e中,用于后面的事件觸發(fā);
最后返回this,這樣就可以鏈?zhǔn)秸{(diào)用了。
once
once方法同樣是用來(lái)監(jiān)聽(tīng)事件的,但是它只會(huì)觸發(fā)一次,代碼如下:
function once(name, callback, ctx) {
var self = this;
function listener() {
self.off(name, listener);
callback.apply(ctx, arguments);
};
listener._ = callback
return this.on(name, listener, ctx);
}
它同on方法一樣,接收三個(gè)參數(shù),最后也是調(diào)用的on方法,但是它會(huì)在on方法的基礎(chǔ)上做一些處理。
它內(nèi)部會(huì)定義一個(gè)listener函數(shù),然后將這個(gè)函數(shù)作為on方法的回調(diào)函數(shù),最后調(diào)用on方法。
listener函數(shù)會(huì)調(diào)用off方法,把自己從事件隊(duì)列中移除,然后再調(diào)用callback函數(shù)。
emit
emit方法是用來(lái)觸發(fā)事件的,代碼如下:
function emit(name) {
var data = [].slice.call(arguments, 1);
var evtArr = ((this.e || (this.e = {}))[name] || []).slice();
var i = 0;
var len = evtArr.length;
for (i; i < len; i++) {
evtArr[i].fn.apply(evtArr[i].ctx, data);
}
return this;
}
它接收一個(gè)參數(shù),就是事件名稱(chēng),然后把除了第一個(gè)參數(shù)以外的參數(shù),轉(zhuǎn)成數(shù)組,存到data中。
然后從this.e中取出name對(duì)應(yīng)的事件隊(duì)列,如果沒(méi)有就創(chuàng)建一個(gè)空數(shù)組,然后把這個(gè)數(shù)組復(fù)制一份,存到evtArr中。
這里老是對(duì)this.e做判斷好麻煩,其實(shí)可以在構(gòu)造函數(shù)中初始化this.e,這樣就不用每次都判斷了。
然后遍歷evtArr,依次調(diào)用每個(gè)事件的回調(diào)函數(shù),把data作為參數(shù)傳進(jìn)去,最后返回this。
off
off方法是用來(lái)移除事件的,代碼如下:
function off(name, callback) {
var e = this.e || (this.e = {});
var evts = e[name];
var liveEvents = [];
if (evts && callback) {
for (var i = 0, len = evts.length; i < len; i++) {
if (evts[i].fn !== callback && evts[i].fn._ !== callback)
liveEvents.push(evts[i]);
}
}
(liveEvents.length)
? e[name] = liveEvents
: delete e[name];
return this;
}
它接收兩個(gè)參數(shù),name和callback,name是事件名稱(chēng),callback是回調(diào)函數(shù)。
它會(huì)從this.e中取出name對(duì)應(yīng)的事件隊(duì)列,然后通過(guò)遍歷,把不等于callback的事件,存到liveEvents中。
最后判斷liveEvents是否為空,如果不為空,就把liveEvents賦值給e[name],否則就刪除e[name]。
動(dòng)手實(shí)現(xiàn)
上面已經(jīng)分析完tiny-emitter的源碼了,我們現(xiàn)在就來(lái)實(shí)現(xiàn)最開(kāi)始提到的Emitter類(lèi)。
當(dāng)然這里還是簡(jiǎn)化版,只有on和emit方法。
class Emitter {
constructor() {
this.events = {};
}
$on(name, callback) {
if (!this.events[name]) {
this.events[name] = [];
}
this.events[name].push(callback);
}
$emit(name, ...args) {
if (this.events[name]) {
this.events[name].forEach(callback => callback(...args));
}
}
}
這里使用ES6的class語(yǔ)法來(lái)實(shí)現(xiàn)這個(gè)功能;
constructor方法用來(lái)初始化events對(duì)象,這樣對(duì)比tiny-emitter的源碼,就不用每次都判斷this.e是否存在了;
$emit通過(guò)...args來(lái)接收除了第一個(gè)參數(shù)以外的參數(shù),就可以不用使用arguments了;
整體來(lái)說(shuō)使用ES6的class語(yǔ)法來(lái)實(shí)現(xiàn),代碼量更少,更加簡(jiǎn)潔,來(lái)看了結(jié)果:

總結(jié)
通過(guò)學(xué)習(xí)tiny-emitter的源碼,我們學(xué)習(xí)到一個(gè)事件總線的設(shè)計(jì)思路以及實(shí)現(xiàn)方式。
了解事件總線的設(shè)計(jì)思路,可以幫助我們更好地理解Vue的事件總線,這可能對(duì)我們使用或者閱讀源碼有幫助。
最后通過(guò)實(shí)現(xiàn)一個(gè)簡(jiǎn)單的事件總線,讓我們加深對(duì)這個(gè)事件總線的理解,也通過(guò)將這個(gè)實(shí)現(xiàn)轉(zhuǎn)換為ES6的class語(yǔ)法,讓我們對(duì)ES6的class語(yǔ)法有更深的理解。
以上就是Vue 組件事件觸發(fā)和監(jiān)聽(tīng)實(shí)現(xiàn)源碼解析的詳細(xì)內(nèi)容,更多關(guān)于Vue 組件事件觸發(fā)監(jiān)聽(tīng)的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
TSX常見(jiàn)簡(jiǎn)單入門(mén)用法之Vue3+Vite
Vue3的確可以直接使用tsx開(kāi)發(fā),唯一需要處理的就是children,而且處理起來(lái)還是比較不爽的,下面這篇文章主要給大家介紹了關(guān)于TSX常見(jiàn)簡(jiǎn)單入門(mén)用法之Vue3+Vite的相關(guān)資料,需要的朋友可以參考下2022-08-08
Vue中Class和Style實(shí)現(xiàn)v-bind綁定的幾種用法
項(xiàng)目開(kāi)發(fā)中給元素添加/刪除 class 是非常常見(jiàn)的行為之一, 例如網(wǎng)站導(dǎo)航都會(huì)給選中項(xiàng)添加一個(gè) active 類(lèi)用來(lái)區(qū)別選與未選中的樣式,那么在 vue 中 我們?nèi)绾翁幚磉@類(lèi)的效果呢?下面我們就一起來(lái)了解一下2021-05-05
Vue 中使用vue2-highcharts實(shí)現(xiàn)top功能的示例
下面小編就為大家分享一篇Vue 中使用vue2-highcharts實(shí)現(xiàn)top功能的示例,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-03-03
vue .js綁定checkbox并獲取、改變選中狀態(tài)的實(shí)例
今天小編就為大家分享一篇vue .js綁定checkbox并獲取、改變選中狀態(tài)的實(shí)例,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-08-08
vue2.0實(shí)現(xiàn)移動(dòng)端的輸入框?qū)崟r(shí)檢索更新列表功能
最近小編在做vue2.0的項(xiàng)目,遇到移動(dòng)端實(shí)時(shí)檢索搜索更新列表的效果,下面腳本之家小編給大家?guī)?lái)了vue2.0 移動(dòng)端的輸入框?qū)崟r(shí)檢索更新列表功能的實(shí)例代碼,感興趣的朋友參考下吧2018-05-05
Vue中ElementUI分頁(yè)組件Pagination的使用方法
這篇文章主要為大家詳細(xì)介紹了Vue中ElementUI分頁(yè)組件Pagination的使用,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-05-05
詳解vue項(xiàng)目打包后通過(guò)百度的BAE發(fā)布到網(wǎng)上的流程
這篇文章主要介紹了將vue的項(xiàng)目打包后通過(guò)百度的BAE發(fā)布到網(wǎng)上的流程,主要運(yùn)用的技術(shù)是vue+express+git+百度的應(yīng)用引擎BAE。需要的朋友可以參考下2018-03-03
Vue使用el-upload批量上傳圖片時(shí)報(bào)錯(cuò)問(wèn)題處理方法
相信大家都知道在element-ui中,el-upload可以進(jìn)行文件多選操作,下面這篇文章主要給大家介紹了關(guān)于Vue使用el-upload批量上傳圖片時(shí)報(bào)錯(cuò)問(wèn)題的處理方法,文中通過(guò)圖文以及實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2023-06-06
深入探討Vue計(jì)算屬性與監(jiān)聽(tīng)器的區(qū)別和用途
在Vue的開(kāi)發(fā)中,計(jì)算屬性(Computed Properties)和監(jiān)聽(tīng)器(Watchers)是兩種非常重要的概念,它們都用于響應(yīng)式地處理數(shù)據(jù)變化,本文將帶你深入了解計(jì)算屬性和監(jiān)聽(tīng)器的區(qū)別,以及在何時(shí)使用它們,感興趣的朋友可以參考下2023-09-09
Vue3+Vite項(xiàng)目使用less的實(shí)現(xiàn)步驟
最近學(xué)習(xí)在vite項(xiàng)目中配置less,本文主要介紹了Vue3+Vite項(xiàng)目使用less的實(shí)現(xiàn)步驟,文中通過(guò)示例代碼介紹的非常詳細(xì),需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2024-02-02

