eventproxy

本文是eventproxy的readme,只是方便我自己偶爾看看的,

這個(gè)世界上不存在所謂回調(diào)函數(shù)深度嵌套的問題。 —— Jackson Tian

世界上本沒有嵌套回調(diào),寫得人多了,也便有了}}}}}}}}}}}}。 —— fengmk2

EventProxy 僅僅是一個(gè)很輕量的工具,但是能夠帶來一種事件式編程的思維變化。有幾個(gè)特點(diǎn):

  1. 利用事件機(jī)制解耦復(fù)雜業(yè)務(wù)邏輯
  2. 移除被廣為詬病的深度callback嵌套問題
  3. 將串行等待變成并行等待,提升多異步協(xié)作場景下的執(zhí)行效率
  4. 友好的Error handling
  5. 無平臺(tái)依賴,適合前后端,能用于瀏覽器和Node.js
  6. 兼容CMD,AMD以及CommonJS模塊環(huán)境

現(xiàn)在的,無深度嵌套的,并行的

var ep = EventProxy.create("template", "data", "l10n", function (template, data, l10n) {
  _.template(template, data, l10n);
});

$.get("template", function (template) {
  // something
  ep.emit("template", template);
});
$.get("data", function (data) {
  // something
  ep.emit("data", data);
});
$.get("l10n", function (l10n) {
  // something
  ep.emit("l10n", l10n);
});

過去的,深度嵌套的,串行的。

var render = function (template, data) {
  _.template(template, data);
};
$.get("template", function (template) {
  // something
  $.get("data", function (data) {
    // something
    $.get("l10n", function (l10n) {
      // something
      render(template, data, l10n);
    });
  });
});

安裝

Node用戶

通過NPM安裝即可使用:

$ npm install eventproxy

調(diào)用:

var EventProxy = require('eventproxy');

spm

$ spm install eventproxy

Component

$ component install JacksonTian/eventproxy

前端用戶

以下示例均指向Github的源文件地址,您也可以下載源文件到你自己的項(xiàng)目中。整個(gè)文件注釋全面,帶注釋和空行,一共約500行。為保證EventProxy的易嵌入,項(xiàng)目暫不提供壓縮版。用戶可以自行采用Uglify、YUI Compressor或Google Closure Complier進(jìn)行壓縮。

普通環(huán)境

在頁面中嵌入腳本即可使用:

<script src="https://raw.github.com/JacksonTian/eventproxy/master/lib/eventproxy.js"></script>

使用:

// EventProxy此時(shí)是一個(gè)全局變量
var ep = new EventProxy();

SeaJS用戶

SeaJS下只需配置別名,然后require引用即可使用。

// 配置
seajs.config({
  alias: {
    eventproxy: 'https://raw.github.com/JacksonTian/eventproxy/master/lib/eventproxy.js'
  }
});
// 使用
seajs.use(['eventproxy'], function (EventProxy) {
  // TODO
});
// 或者
define('test', function (require, exports, modules) {
  var EventProxy = require('eventproxy');
});

RequireJS用戶

RequireJS實(shí)現(xiàn)的是AMD規(guī)范。

// 配置路徑
require.config({
  paths: {
    eventproxy: "https://raw.github.com/JacksonTian/eventproxy/master/lib/eventproxy"
  }
});
// 使用
require(["eventproxy"], function (EventProxy) {
  // TODO
});

異步協(xié)作

多類型異步協(xié)作

此處以頁面渲染為場景,渲染頁面需要模板、數(shù)據(jù)。假設(shè)都需要異步讀取。

var ep = new EventProxy();
ep.all('tpl', 'data', function (tpl, data) { // or ep.all(['tpl', 'data'], function (tpl, data) {})
  // 在所有指定的事件觸發(fā)后,將會(huì)被調(diào)用執(zhí)行
  // 參數(shù)對應(yīng)各自的事件名
});
fs.readFile('template.tpl', 'utf-8', function (err, content) {
  ep.emit('tpl', content);
});
db.get('some sql', function (err, result) {
  ep.emit('data', result);
});

all方法將handler注冊到事件組合上。當(dāng)注冊的多個(gè)事件都觸發(fā)后,將會(huì)調(diào)用handler執(zhí)行,每個(gè)事件傳遞的數(shù)據(jù),將會(huì)依照事件名順序,傳入handler作為參數(shù)。

快速創(chuàng)建

EventProxy提供了create靜態(tài)方法,可以快速完成注冊all事件。

var ep = EventProxy.create('tpl', 'data', function (tpl, data) {
  // TODO
});

以上方法等效于

var ep = new EventProxy();
ep.all('tpl', 'data', function (tpl, data) {
  // TODO
});

重復(fù)異步協(xié)作

此處以讀取目錄下的所有文件為例,在異步操作中,我們需要在所有異步調(diào)用結(jié)束后,執(zhí)行某些操作。

var ep = new EventProxy();
ep.after('got_file', files.length, function (list) {
  // 在所有文件的異步執(zhí)行結(jié)束后將被執(zhí)行
  // 所有文件的內(nèi)容都存在list數(shù)組中
});
for (var i = 0; i < files.length; i++) {
  fs.readFile(files[i], 'utf-8', function (err, content) {
    // 觸發(fā)結(jié)果事件
    ep.emit('got_file', content);
  });
}

after方法適合重復(fù)的操作,比如讀取10個(gè)文件,調(diào)用5次數(shù)據(jù)庫等。將handler注冊到N次相同事件的觸發(fā)上。達(dá)到指定的觸發(fā)數(shù),handler將會(huì)被調(diào)用執(zhí)行,每次觸發(fā)的數(shù)據(jù),將會(huì)按觸發(fā)順序,存為數(shù)組作為參數(shù)傳入。

持續(xù)型異步協(xié)作

此處以股票為例,數(shù)據(jù)和模板都是異步獲取,但是數(shù)據(jù)會(huì)持續(xù)刷新,視圖會(huì)需要重新刷新。

var ep = new EventProxy();
ep.tail('tpl', 'data', function (tpl, data) {
  // 在所有指定的事件觸發(fā)后,將會(huì)被調(diào)用執(zhí)行
  // 參數(shù)對應(yīng)各自的事件名的最新數(shù)據(jù)
});
fs.readFile('template.tpl', 'utf-8', function (err, content) {
  ep.emit('tpl', content);
});
setInterval(function () {
  db.get('some sql', function (err, result) {
    ep.emit('data', result);
  });
}, 2000);

tailall方法比較類似,都是注冊到事件組合上。不同在于,指定事件都觸發(fā)之后,如果事件依舊持續(xù)觸發(fā),將會(huì)在每次觸發(fā)時(shí)調(diào)用handler,極像一條尾巴。

基本事件

通過事件實(shí)現(xiàn)異步協(xié)作是EventProxy的主要亮點(diǎn)。除此之外,它還是一個(gè)基本的事件庫。攜帶如下基本API

  • on/addListener,綁定事件監(jiān)聽器
  • emit,觸發(fā)事件
  • once,綁定只執(zhí)行一次的事件監(jiān)聽器
  • removeListener,移除事件的監(jiān)聽器
  • removeAllListeners,移除單個(gè)事件或者所有事件的監(jiān)聽器

為了照顧各個(gè)環(huán)境的開發(fā)者,上面的方法多具有別名。

  • YUI3使用者,subscribefire你應(yīng)該知道分別對應(yīng)的是on/addListeneremit
  • jQuery使用者,trigger對應(yīng)的方法是emit,bind對應(yīng)的就是on/addListener。
  • removeListenerremoveAllListeners其實(shí)都可以通過別名unbind完成。

所以在你的環(huán)境下,選用你喜歡的API即可。

更多API的描述請?jiān)L問API Docs。

異常處理

在異步方法中,實(shí)際上,異常處理需要占用一定比例的精力。在過去一段時(shí)間內(nèi),我們都是通過額外添加error事件來進(jìn)行處理的,代碼大致如下:

exports.getContent = function (callback) {
 var ep = new EventProxy();
  ep.all('tpl', 'data', function (tpl, data) {
    // 成功回調(diào)
    callback(null, {
      template: tpl,
      data: data
    });
  });
  // 偵聽error事件
  ep.bind('error', function (err) {
    // 卸載掉所有handler
    ep.unbind();
    // 異?;卣{(diào)
    callback(err);
  });
  fs.readFile('template.tpl', 'utf-8', function (err, content) {
    if (err) {
      // 一旦發(fā)生異常,一律交給error事件的handler處理
      return ep.emit('error', err);
    }
    ep.emit('tpl', content);
  });
  db.get('some sql', function (err, result) {
    if (err) {
      // 一旦發(fā)生異常,一律交給error事件的handler處理
      return ep.emit('error', err);
    }
    ep.emit('data', result);
  });
};

代碼量因?yàn)楫惓5奶幚?,一下子上去了很多。在這里EventProxy經(jīng)過很多實(shí)踐后,我們根據(jù)我們的最佳實(shí)踐提供了優(yōu)化的錯(cuò)誤處理方案。

exports.getContent = function (callback) {
 var ep = new EventProxy();
  ep.all('tpl', 'data', function (tpl, data) {
    // 成功回調(diào)
    callback(null, {
      template: tpl,
      data: data
    });
  });
  // 添加error handler
  ep.fail(callback);

  fs.readFile('template.tpl', 'utf-8', ep.done('tpl'));
  db.get('some sql', ep.done('data'));
};

上述代碼優(yōu)化之后,業(yè)務(wù)開發(fā)者幾乎不用關(guān)心異常處理了。代碼量降低效果明顯。
這里代碼的轉(zhuǎn)換,也許有開發(fā)者并不放心。其實(shí)秘訣在fail方法和done方法中。

神奇的fail

ep.fail(callback);
// 由于參數(shù)位相同,它實(shí)際是
ep.fail(function (err) {
  callback(err);
});

// 等價(jià)于
ep.bind('error', function (err) {
  // 卸載掉所有handler
  ep.unbind();
  // 異常回調(diào)
  callback(err);
});

fail方法偵聽了error事件,默認(rèn)處理卸載掉所有handler,并調(diào)用回調(diào)函數(shù)。

神奇的 throw

throwep.emit('error', err) 的簡寫。

var err = new Error();
ep.throw(err);
// 實(shí)際是
ep.emit('error', err);

神奇的done

ep.done('tpl');
// 等價(jià)于
function (err, content) {
  if (err) {
    // 一旦發(fā)生異常,一律交給error事件的handler處理
    return ep.emit('error', err);
  }
  ep.emit('tpl', content);
}

在Node的最佳實(shí)踐中,回調(diào)函數(shù)第一個(gè)參數(shù)一定會(huì)是一個(gè)error對象。檢測到異常后,將會(huì)觸發(fā)error事件。剩下的參數(shù),將觸發(fā)事件,傳遞給對應(yīng)handler處理。

done也接受回調(diào)函數(shù)

done方法除了接受事件名外,還接受回調(diào)函數(shù)。如果是函數(shù)時(shí),它將剔除第一個(gè)error對象(此時(shí)為null)后剩余的參數(shù),傳遞給該回調(diào)函數(shù)作為參數(shù)。該回調(diào)函數(shù)無需考慮異常處理。

ep.done(function (content) {
  // 這里無需考慮異常
  // 手工emit
  ep.emit('someevent', newcontent);
});

當(dāng)然手工emit的方式并不太好,我們更進(jìn)一步的版本:

ep.done('tpl', function (tpl) {
  // 將內(nèi)容更改后,返回即可
  return tpl.trim();
});

注意事項(xiàng)

如果emit需要傳遞多個(gè)參數(shù)時(shí),ep.done(event, fn)的方式不能滿足需求,還是需要ep.done(fn),進(jìn)行手工emit多個(gè)參數(shù)。

神奇的group

fail除了用于協(xié)助all方法完成外,也能協(xié)助after中的異常處理。另外,在after的回調(diào)函數(shù)中,結(jié)果順序是與用戶emit的順序有關(guān)。為了滿足返回?cái)?shù)據(jù)按發(fā)起異步調(diào)用的順序排列,EventProxy提供了group方法。

var ep = new EventProxy();
ep.after('got_file', files.length, function (list) {
  // 在所有文件的異步執(zhí)行結(jié)束后將被執(zhí)行
  // 所有文件的內(nèi)容都存在list數(shù)組中,按順序排列
});
for (var i = 0; i < files.length; i++) {
  fs.readFile(files[i], 'utf-8', ep.group('got_file'));
}

group秉承done函數(shù)的設(shè)計(jì),它包含異常的傳遞。同時(shí)它還隱含了對返回?cái)?shù)據(jù)進(jìn)行編號(hào),在結(jié)束時(shí),按順序返回。

ep.group('got_file');
// 約等價(jià)于
function (err, data) {
  if (err) {
    return ep.emit('error', err);
  }
  ep.emit('got_file', data);
};

當(dāng)回調(diào)函數(shù)的數(shù)據(jù)還需要進(jìn)行加工時(shí),可以給group帶上回調(diào)函數(shù),只要在操作后將數(shù)據(jù)返回即可:

ep.group('got_file', function (data) {
  // some code
  return data;
});

異步事件觸發(fā): emitLater && doneLater

在node中,emit方法是同步的,EventProxy中的emit,trigger等跟node的風(fēng)格一致,也是同步的??聪旅孢@段代碼,可能眼尖的同學(xué)一下就發(fā)現(xiàn)了隱藏的bug:

var ep = EventProxy.create();

db.check('key', function (err, permission) {
  if (err) {
    return ep.emit('error', err);
  }
  ep.emit('check', permission);
});

ep.once('check', function (permission) {
  permission && db.get('key', function (err, data) {
    if (err) {
      return ep.emit('error');
    }
    ep.emit('get', data);
  });
});

ep.once('get', function (err, data) {
  if (err) {
    return ep.emit('error', err);
  }
  render(data);
});

ep.on('error', errorHandler);

沒錯(cuò),萬一db.checkcallback被同步執(zhí)行了,在ep監(jiān)聽check事件之前,它就已經(jīng)被拋出來了,后續(xù)邏輯沒辦法繼續(xù)執(zhí)行。盡管node的約定是所有的callback都是需要異步返回的,但是如果這個(gè)方法是由第三方提供的,我們沒有辦法保證db.checkcallback一定會(huì)異步執(zhí)行,所以我們的代碼通常就變成了這樣:

var ep = EventProxy.create();

ep.once('check', function (permission) {
  permission && db.get('key', function (err, data) {
    if (err) {
      return ep.emit('error');
    }
    ep.emit('get', data);
  });
});

ep.once('get', function (err, data) {
  if (err) {
    return ep.emit('error', err);
  }
  render(data);
});

ep.on('error', errorHandler);

db.check('key', function (err, permission) {
  if (err) {
    return ep.emit('error', err);
  }
  ep.emit('check', permission);
});

我們被迫把db.check挪到最后,保證事件先被監(jiān)聽,再執(zhí)行db.checkcheck->get->render的邏輯,在代碼中看起來變成了get->render->check。如果整個(gè)邏輯更加復(fù)雜,這種風(fēng)格將會(huì)讓代碼很難讀懂。

這時(shí)候,我們需要的就是 異步事件觸發(fā)

var ep = EventProxy.create();

db.check('key', function (err, permission) {
  if (err) {
    return ep.emitLater('error', err);
  }
  ep.emitLater('check', permission);
});

ep.once('check', function (permission) {
  permission && db.get('key', function (err, data) {
    if (err) {
      return ep.emit('error');
    }
    ep.emit('get', data);
  });
});

ep.once('get', function (err, data) {
  if (err) {
    return ep.emit('error', err);
  }
  render(data);
});

ep.on('error', errorHandler);

上面代碼中,我們把db.check的回調(diào)函數(shù)中的事件通過emitLater觸發(fā),這樣,就算db.check的回調(diào)函數(shù)被同步執(zhí)行了,事件的觸發(fā)也還是異步的,ep在當(dāng)前事件循環(huán)中監(jiān)聽了所有的事件,之后的事件循環(huán)中才會(huì)去觸發(fā)check事件。代碼順序?qū)⒑瓦壿嬳樞虮3忠恢隆?br> 當(dāng)然,這么復(fù)雜的代碼,必須可以像ep.done()一樣通過doneLater來解決:

var ep = EventProxy.create();

db.check('key', ep.doneLater('check'));

ep.once('check', function (permission) {
  permission && db.get('key', ep.done('get'));
});

ep.once('get', function (data) {
  render(data);
});

ep.fail(errorHandler);

最終呈現(xiàn)出來的,是一段簡潔且清晰的代碼。

注意事項(xiàng)

  • 請勿使用all作為業(yè)務(wù)中的事件名。該事件名為保留事件。
  • 異常處理部分,請遵循Node的最佳實(shí)踐(回調(diào)函數(shù)首個(gè)參數(shù)為異常傳遞位)。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容