我們知道,在JavaScript中,原生DOM事件在開發中是很有用的(與用戶交互的重要方式),但是操作原生DOM事件其實有兩大缺點:性能低、依賴於瀏覽器(NodeJs、小程序等不可用)。那麼這個時候,就需要我們進行自定義事件去處理某些特定的業務。
認識Event對象及元素的dispatchEvent方法
在JavaScript中,所有事件的父對象就是Event對象,也就是說像我們平時所有的點擊(click)、觸摸(touch)、鼠標等事件對象都繼承自Event。理解這一點是很重要的。先來簡單看一個事件的場景。
場景一、頁面上有兩個按鈕a、b,當點擊按鈕b的時候,調用按鈕a的點擊事件。簡單佈局代碼如下:
<button id="btn1">a</button>
<button id="btn2">b</button>
程序員A的做法,分別獲取這兩個按鈕,然後給b按鈕添加點擊事件後,調用按鈕a的click方法。代碼如下:
<button id="btn1" onclick="alert('I am a button named a')">a</button>
<button id="btn2">b</button>
<script>
let btn1 = document.querySelector('#btn1');
let btn2 = document.querySelector('#btn2');
btn2.onclick = function(){
btn1.onclick();
}
</script>
程序員B的做法,分別獲取這兩個按鈕,然後給b按鈕添加點擊事件後,在回調函數中在添加按鈕a的點擊事件。代碼如下:
<button id="btn1">a</button>
<button id="btn2">b</button>
<script>
let btn1 = document.querySelector('#btn1');
let btn2 = document.querySelector('#btn2');
btn2.onclick = function(){
btn1.addEventListener('click',function(){
alert('I am a button named a')
},false)
}
</script>
看到這裏,你認爲誰的做法是正確的?顯然程序員A的做法是對的(就目前的要求來看),但有缺陷,如果按鈕a的事件是通過addEventListener方法去註冊監聽的,就不起作用了。那麼該怎樣做纔會比較好?這就需要我們的Event對象和元素的dispatchEvent方法了。改進代碼如下:
<button id="btn1">a</button>
<button id="btn2">b</button>
<script>
let btn1 = document.querySelector('#btn1');
let btn2 = document.querySelector('#btn2');
btn1.addEventListener('click',function(){
alert('I am a button named a')
},false)
btn2.onclick = function(){
let ev = new Event('click');
btn1.dispatchEvent(ev);
}
</script>
其中:
- Event對象的構造函數需要一個參數,事件類型
- dispatchEvent方法是將某個事件分發給某個元素
認識自定義事件
前面說過,在瀏覽器端javascript中,所有的事件都繼承自Event,那麼其實要想實現一個自定義事件,也是需要繼承自Event。
還是類似上面說過的場景,場景二:有兩個按鈕a,b,當調用b按鈕的點擊事件,怎麼去觸發a按鈕上的自定義事件?
<button id="btn1">a</button>
<button id="btn2">b</button>
<script>
let a = document.querySelector('#btn1');
let b = document.querySelector('#btn2');
a.addEventListener('myClick',function(){
alert('I am a button named a')
},false)
class MyEvent extends Event{
constructor(...args){
super(...args);
this.a = 12;
}
}
b.onclick = function(){
const ev = new MyEvent('myClick');
a.dispatchEvent(ev);
}
</script>
這就是自定義事件的思想體現:
- 根據事件類型進行事件的註冊
- 根據事件的類型分發對應的事件給需要者
可以看出,對事件進行管理是很有必要,如Java中的EventBus、VueJs中的$on、$emit等,將事件的監聽者及分發者抽象成一個獨立的模塊,來進行事件的管理(添加、移除等)有利用解耦。
事件隊列完成組件間的通信
這裏可以把事件隊列想象成一根管道,類似前端gulp實現的核心思想(基於管道)、當使用者需要使用某個事件的時候,就在管道中註冊一個事件,然後通過該事件的類型,從管道中分發一個該類型的事件,在不需要使用後,還可以對相應的事件進行移除操作。代碼如下:
class Pipe{
constructor(){
this.pipes = {};
}
/**
* 註冊事件
* @param {*} type
* @param {*} fn
*/
on(type,fn){
this.pipes[type] = this.pipes[type] || [];
if(this.pipes[type].findIndex(func => func==fn)==-1){
this.pipes[type].push(fn);
}
}
/**
* 移除事件
* @param {*} type
* @param {*} fn
*/
off(type,fn){
if(this.pipes[type]){
this.pipes[type] = this.pipes[type].filter((func) => func!==fn);
if(this.pipes[type].length===0){
delete this.pipes[type];
}
}
}
/**
* 分發事件
* @param {*} type
* @param {...any} args
*/
emit(type,...args){
if(this.pipes[type]){
this.pipes[type].forEach((fn) => {
fn(...args);
})
}
}
}
場景:通過事件隊列模擬vuejs中組件間通信的實現。Component1負責對數據進行渲染,Component2中點擊按鈕,來改變Component1實例屬性a的值。代碼如下:
<div id="box">
{{a}}
</div>
<button id="btn1">++</button>
<script>
const $ = document.querySelectorAll.bind(document);
let pipe = new Pipe();
class Component1{
constructor(){
this.a = 12;
this.el = $("#box")[0];
this.render();
pipe.on('add',(arg) => {
this.a+=arg;
this.render();
})
}
render(){
this.el.innerHTML = this.a;
}
}
class Component2{
constructor(){
this.el = $("#btn1")[0];
this.el.onclick = function(){
pipe.emit('add',12);
}
}
}
new Component1();
new Component2();
</script>