Christmas Trees, Promises和Event Emitters

Jaxu發表於2016-07-06

  今天有同事問我下面這段程式碼是什麼意思:

var MyClass = function() {
  events.EventEmitter.call(this); // 這行是什麼意思?
};
util.inherits(MyClass, events.EventEmitter); // 還有這行?

  我也不是很明白,於是研究了一下。下面是我的一些體會。

Christmas Trees和Errors

  如果你寫過JavaScript或NodeJS程式碼,你也許會對callback地獄深有體會。每次當你進行非同步呼叫時,按照callback的契約,你需要傳一個function作為回撥函式,function的第一個引數則預設為接收的error。這是一個非常棒的約定,不過仍然存在兩個小問題:

  1. 每次回撥過程中都需要檢查是否存在error - 這很煩人

  2. 每一次的回撥都會使程式碼向右縮排,如果回撥的層級很多,則我們的程式碼看起來就像聖誕樹一樣:

  此外,如果每個回撥都是一個匿名函式幷包含大量的程式碼,那麼維護這樣的程式碼將會使人抓狂。

你會怎麼做呢?

  其實有許多方法都可以解決這些問題,下面我將提供三種不同方式編寫的程式碼用於說明它們之間的區別。

  1. 標準回撥函式

  2. Event Emitter

  3. Promises

  我建立了一個簡單的類"User Registration",用於將email儲存到資料庫併傳送。在每個示例中,我都假設save操作成功,傳送email操作失敗。

  1)標準回撥函式

  前面已經提過,NodeJS對回撥函式有一個約定,那就是error在前,回撥在後。在每一次回撥中,如果出現錯誤,你需要將錯誤丟擲並截斷餘下的回撥操作。

########################### registration.js #################################
var Registration = function () {
  if (!(this instanceof Registration)) return new Registration();
  var _save = function (email, callback) {
    setTimeout(function(){
      callback(null);
    }, 20);
  };
  var _send = function (email, callback) {
    setTimeout(function(){
      callback(new Error("Failed to send"));
    }, 20);
  };
  this.register = function (email, callback) {
    _save(email, function (err) {
      if (err)
        return callback(err);
      _send(email, function (err) {
        callback(err);
      });
    });
  };
};
module.exports = Registration;
########################### app.js #################################
var Registration = require('./registration.js');
var registry = new Registration();
registry.register("john@example.com", function (err) {
  console.log("done", err);
});

  大部分時候我還是傾向於使用標準回撥函式。如果你覺得你的程式碼結構看起來很清晰,那麼我不認為"Christmas Tree"會對我產生太多的困擾。回撥中的error檢查會有點煩人,不過程式碼看起來很簡單。

  2)Event Emitter

  在NodeJS中,有一個內建的庫叫EventEmitter非常不錯,它被廣泛應用到NodeJS的整個系統中。

  你可以建立emitter的一個例項,不過更常見的做法是從emitter繼承,這樣你可以訂閱從特定物件上產生的事件。

  最關鍵的是我們可以將事件連線起來變成一種工作流如“當email被成功儲存之後就立刻傳送”。

  此外,名為error的事件有一種特殊的行為,當error事件沒有被任何物件訂閱時,它將在控制檯列印堆疊跟蹤資訊並退出整個程式。也就是說,未處理的errors會使整個程式崩掉。

########################### registration.js #################################
var util = require('util');
var EventEmitter = require('events').EventEmitter;
var Registration = function () {
  //call the base constructor
  EventEmitter.call(this);
  var _save = function (email, callback) {
    this.emit('saved', email);
  };
  var _send = function (email, callback) {
    //or call this on success: this.emit('sent', email);
    this.emit('error', new Error("unable to send email"));
  };
  var _success = function (email, callback) {
    this.emit('success', email);
  };
  //the only public method
  this.register = function (email, callback) {
    this.emit('beginRegistration', email);
  };
  //wire up our events
  this.on('beginRegistration', _save);
  this.on('saved', _send);
  this.on('sent', _success);
};
//inherit from EventEmitter
util.inherits(Registration, EventEmitter);
module.exports = Registration;
########################### app.js #################################
var Registration = require('./registration.js');
var registry = new Registration();
//if we didn't register for 'error', then the program would close when an error happened
registry.on('error', function(err){
  console.log("Failed with error:", err);
});
//register for the success event
registry.on('success', function(){
  console.log("Success!");
});
//begin the registration
registry.register("john@example.com");

  你可以看到上面的程式碼中幾乎沒有什麼巢狀,而且我們也不用像之前那樣在回撥函式中去檢查errors。如果有錯誤發生,程式將丟擲錯誤資訊並繞過餘下的註冊過程。

  3)Promises

  這裡有大量關於promises的說明,如promises-spec, common-js等等。下面是我的理解。

  Promises是一種約定,它使得對巢狀回撥和錯誤的管理看起來更加優雅。例如非同步呼叫一個save方法,我們不用給它傳遞迴調函式,該方法將返回一個promise物件。這個物件包含一個then方法,我們將callback函式傳遞給它以完成回撥函式的註冊。

  據我所知,人們傾向於使用標準回撥函式的形式來編寫程式碼,然後使用如deferred的庫來將這些回撥函式轉換成promise的形式。這正是我在這裡要做的。

######################### app.js ############################
var Registration = require('./registration.js');
var registry = new Registration();
registry.register("john@example.com")
  .then(
    function () {
      console.log("Success!");
    },
    function (err) {
      console.log("Failed with error:", err);
    }
  );
######################### registration.js ############################
var deferred = require('deferred');
var Registration = function () {
  //written as conventional callbacks, then converted to promises
  var _save = deferred.promisify(function (email, callback) {
    callback(null);
  });
  var _send = deferred.promisify(function (email, callback) {
    callback(new Error("Failed to send"));
  });
  this.register = function (email, callback) {
    //chain two promises together and return a promise
    return _save(email)
             .then(_send);
  };
};
module.exports = Registration;

  promise消除了程式碼中的巢狀呼叫以及像EventEmitter那樣傳遞error。這裡我列出了它們之間的一些區別:

  EventEmitter

  • 需要引用util和events庫,這兩個庫已經包含在NodeJS中
  • 你需要將自己的類從EventEmitter繼承
  • 如果error未處理則會丟擲執行時異常
  • 支援釋出/訂閱模式

  Promise

  • 使整個回撥形成一個鏈式結構
  • 需要庫的支援來將回撥函式轉換成promise的形式,如deferred或Q
  • 會帶來更多的開銷,所以可能會稍微有點慢
  • 不支援釋出/訂閱模式

原文地址:http://www.joshwright.com/tips/javascript-christmas-trees-promises-and-event-emitters

相關文章