本文是eventproxy的readme,只是方便我自己偶爾看看的,
這個(gè)世界上不存在所謂回調(diào)函數(shù)深度嵌套的問題。 —— Jackson Tian
世界上本沒有嵌套回調(diào),寫得人多了,也便有了
}}}}}}}}}}}}。 —— fengmk2
- API文檔: API Documentation
- jscoverage: 97%
- 源碼注解:注解文檔
EventProxy 僅僅是一個(gè)很輕量的工具,但是能夠帶來一種事件式編程的思維變化。有幾個(gè)特點(diǎn):
- 利用事件機(jī)制解耦復(fù)雜業(yè)務(wù)邏輯
- 移除被廣為詬病的深度callback嵌套問題
- 將串行等待變成并行等待,提升多異步協(xié)作場景下的執(zhí)行效率
- 友好的Error handling
- 無平臺(tái)依賴,適合前后端,能用于瀏覽器和Node.js
- 兼容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);
tail與all方法比較類似,都是注冊到事件組合上。不同在于,指定事件都觸發(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使用者,
subscribe和fire你應(yīng)該知道分別對應(yīng)的是on/addListener和emit。 - jQuery使用者,
trigger對應(yīng)的方法是emit,bind對應(yīng)的就是on/addListener。 -
removeListener和removeAllListeners其實(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
throw 是 ep.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.check的callback被同步執(zhí)行了,在ep監(jiān)聽check事件之前,它就已經(jīng)被拋出來了,后續(xù)邏輯沒辦法繼續(xù)執(zhí)行。盡管node的約定是所有的callback都是需要異步返回的,但是如果這個(gè)方法是由第三方提供的,我們沒有辦法保證db.check的callback一定會(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.check。check->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ù)為異常傳遞位)。