深入nodejs-核心模塊Events詳解(事件驅動)

事件驅動

Node.js 是一個基於 Chrome V8 引擎的 JavaScript 運行環境。Node.js 使用了一個事件驅動、非阻塞式 I/O的模型,使其輕量又高效。Allows you to build scalable network applications usingJavaScript on the server-side.

對於上面這段官方的引用大家應該都看過,nodejs是基於事件驅動和非阻塞I/O的方式來設計運行的,那麼作爲實現事件驅動的核心模塊Events就成了深入學習node.js的關鍵。在node中大部分的模塊的實現都繼承了Events類。 比如,文件操作中的fs事件流,網絡編程所用到的tcp,http模塊等,當你回想自己寫的程序後,會發現很多操作都基於事件驅動,Events類。

那麼問題是什麼是事件驅動呢?

簡單來說,就是通過監聽事件的狀態變化來做出相應的操作。比如讀取一個文件,文件讀取完畢,或者文件讀取錯誤,那麼就觸發對應的狀態,然後調用對應的回掉函數來進行處理。

我們來簡單的看幾段代碼來回憶一下:

    const fs = require('fs');
    
    let rs = fs.createReadStream('1.txt');
    
    // 監聽文件打開操作
    rs.on('open', function() {
      console.log('open');
    });
    
    // 監聽數據流讀取
    rs.on('data', function(data) {
      console.log(data);
    });
    
    // 監聽錯誤
    rs.on('error', function() {
      console.log('error');
    });
    
    // 監聽讀取結束操作
    rs.on('end', function() {
      console.log('end');
    });
    
    
    // 監聽文件關閉操作
    rs.on('close', function() {
      console.log('close');
    });

上面這段在創建文件讀取流的操作上,針對文件的打開,數據,錯誤,結束,關閉等幾個狀態進行了監聽的回調處理,這也應徵了我們上面的定義通過監聽事件的狀態變化來做出相應的操作。
那麼這些監聽事件又是如何觸發的呢?它是通過Events類中的emit方法去發射事件的。那麼下面我們來看一下Events是如何實現這樣的監聽操作的。

Events類

主要的幾個核心API

  • on(eventName, listener)和emitter.addListener(eventName, listener):對指定事件綁定事件處理函數
  • once(eventName, listener):對指定事件指定只執行一次的事件處理函數
  • emit(eventName[, ...args]): 觸發指定事件
  • removeListener(eventName, listener):對指定事件解除事件處理函數
  • removeAllListeners([event]):對指定的事件接觸所有的事件處理函數
  • setMaxListeners 設置最大隊列的長度

下面來看一下代碼,看看是怎麼使用的

    const events = require('events');
    
    const EventsEmitter = new events();
    
    //===============事件監聽部分===============
    EventsEmitter.on('open', function() {
      console.log('open');
    });
    
    EventsEmitter.on('data', function(data) {
      console.log(data);
    });
    
    EventsEmitter.on('error', function() {
      console.log('error');
    });
    
    EventsEmitter.on('end', function() {
      console.log('end');
    });
    
    EventsEmitter.on('close', function() {
      console.log('close');
    });
    
    //=============事件觸發部分=================
    
    // 觸發open事件
    EventsEmitter.emit('open');
    // 觸發data事件,並傳遞一個字符串參數'test'
    EventsEmitter.emit('data','test');
    // 觸發error事件
    EventsEmitter.emit('error');
    // 觸發end事件
    EventsEmitter.emit('end');
    // 觸發close事件
    EventsEmitter.emit('close');

看完上面這段代碼是不是更進一步的理解了呢。我們回顧最最上面的那段文件流監聽的代碼,其實就是文件在不同的狀態下去發射相應的emit事件。 而在那段代碼中我們並沒有去引入events這個node提供的模塊,是因爲文件流中繼承了events模塊,所以rs這個變量也就擁有了相應的rs.on()這個方法了。

看到這裏我想應該都瞭解的差不多了。那麼下面來試着實現一下Events這個類,加深理解。

初始化Events模塊

  • 創建一個Events類
  • 初始化this.events用來保存我們需要監聽的事件
  • 將模塊導出
class Events {
  constructor() {
    this.events = {};
  }
}

module.exports = Events;

實現Events.on方法

  • on方法接收兩個參數:

    • type:監聽的事件類型
    • listener:回調函數
  • 將對應的事件先存放在一個對象中,分兩種情況:

    • 該事件對象不存在,那麼以type爲key,[listener]爲值存放在實現初始化好的this.events對象中(注意這裏存的是一個數組,例如data事件,this.events = {data:[callback]})
    • 如果該事件已經存在則直接push
    • 監聽函數就這麼簡單的實現了,接下來就是等着被emit觸發了。
  /**
   * 事件監聽
   * @param {*} type        監聽的事件類型
   * @param {*} listener    回調函數
   */
  on(type, listener) {
    if (this.events[type]) {
      this.events[type].push(listener);
    } else {
      this.events[type] = [listener];
    }
  }

這裏在補充一下,同一個監聽事件是可以添加多個的,所以這裏纔會this.events[type]纔會給一個數組來存儲

實現Events.emit方法

  • 接收兩個參數:

    • type:要觸發的事件類型
    • ...rest:若干個參數,傳遞給對應事件的回調函數
  • 通過type,在this.events裏找到相應的事件,這裏我們上面是存成了一個數組,裏面對應的是事件的回調好書。
  • 循環數組,執行所有對應事件的回調。
  /**
   * 事件觸發
   * @param {*} type        要觸發的事件類型
   * @param  {...any} rest  接收到的若干個參數,這個參數會作爲參數被傳遞到對應事件的回調函數中
   */
  emit(type, ...rest) {
    if (this.events[type]) {
      this.events[type].forEach(listener => {
        listener.apply(this, rest);
      });
    }
  }

寫到這裏,我們之前的代碼就能夠引入自己寫的這個Events模塊來執行了

實現Events.removeListener方法

這個方法是用來刪除對應事件的某個監聽函數,那麼我們只需要把該事件從this.events[type]中刪除即可

  • 接收兩個參數:

    • type:事件類型
    • listener:要刪除的監聽函數
  • 通過this.events[type]找到對應的事件監聽函數數組
  • 通過filter對要刪除的監聽函數進行過濾
  /**
   * 刪除指定事件中的監聽函數
   * @param {*} type      對應的事件 
   * @param {*} listener  要刪除的監聽函數
   */
  removeListener(type, listener) {
    if (this.events[type]) {
      this.events[type].filter(l => l !== listener);
    }
  }  

實現Events.once方法

這個方法和on一樣,唯一的區別就是它只會執行一次,即便多次調用emit去觸發相同的事件監聽,它也只會執行一次。

  • 實現方式其實就是對on()方法做了一層封裝,將一個封裝好的wraper代替listener傳遞給on()方法
  • wraper內部會執行一次監聽回調函數,然後再調用this.removeListern對該回調進行刪除。
  /**
   * 事件監聽,但是隻執行一次
   * @param {*} type        監聽的事件類型
   * @param {*} listener    回調函數
   */
  once(type, listener) {
    const wraper = (...rest) => {
      listener.apply(this, rest);
      this.removeListener(type, wraper);
    };
    this.on(type, wrapper);
  }

完整代碼

class Events {
  constructor() {
    this.events = {};
  }
  /**
   * 事件監聽
   * @param {*} type        監聽的事件類型
   * @param {*} listener    回調函數
   */
  on(type, listener) {
    if (this.events[type]) {
      this.events[type].push(listener);
    } else {
      this.events[type] = [listener];
    }
  }

  /**
   * 事件監聽,但是隻執行一次
   * @param {*} type        監聽的事件類型
   * @param {*} listener    回調函數
   */
  once(type, listener) {
    const wraper = (...rest) => {
      listener.apply(this, rest);
      this.removeListener(type, wraper);
    };
    this.on(type, wrapper);
  }

  /**
   * 事件觸發
   * @param {*} type        要觸發的事件類型
   * @param  {...any} rest  接收到的若干個參數,這個參數會作爲參數被傳遞到對應事件的回調函數中
   */
  emit(type, ...rest) {
    if (this.events[type]) {
      this.events[type].forEach(listener => {
        listener.apply(this, rest);
      });
    }
  }

  /**
   * 刪除指定事件中的監聽函數
   * @param {*} type      對應的事件
   * @param {*} listener  要刪除的監聽函數
   */
  removeListener(type, listener) {
    if (this.events[type]) {
      this.events[type].filter(l => l !== listener);
    }
  }
}

module.exports = Events;

總結

Events模塊是node非常核心的模塊,它對你深入去學習node有很大的幫助,上面我只寫了幾個方法,內部還有幾個API以及一些非常細節的地方可以自己試着去擴展,我這裏就不一個一個的去寫了,文章有寫不好的地方或者看不懂的地方都可以給我留言哦。

如果覺得寫的還行,方便的話幫忙點個贊哦,謝謝了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章