从事件驱动到observable的异步编程——PubSub+Promise+Rx的JS事件库
你上当叻,虽然从外面看标题很有气势,传达出一种宏大叙事的赶脚,其实我只是刚刚把一个阿尔法城的JS模块提交到github,想顺便介绍一下,但我连API文档都懒得写,就别指望能深入浅出的讲一遍来龙去脉了⋯⋯
所以就直接帖几个前置阅读的链接罢!
这些潮流的外部起源:(技术也有外源论/exogenesis⋯⋯)
Twisted(Python的事件驱动异步引擎)里的Deferred模式
微软推崇的Reactive Extensions (Rx)
虽然我是微软黑但微软网站上的这两篇推介文章不错:
Understanding the Publish/Subscribe Pattern for Greater JavaScript Scalability
Asynchronous Programming in JavaScript with “Promises”
应该都有人翻译了,比如这个:infoQ: JavaScript异步编程的Promise模式
jQuery早就跟微软一个鼻孔出气了:
http://api.jquery.com/category/deferred-object/
CommonJS的Promises提案,照例又分了好多种ABCD神马的:
http://wiki.commonjs.org/wiki/Promises
阿尔法城的客户端程序里有一个叫作event的模块提供了以上提到的PubSub模式、Promise模式和部分Rx模式,可以算是OzJS的核心module。
就像名字一样,它的初衷是一个最基础最简洁的消息事件库,类似nodejs的EventEmitter。在项目实践中,我很早就注意到可以用统一的事件机制实现Twisted风格的API,为此需要能随时提取事件主题(本质上就是promise对象),后来又根据实际需求加入了能表示状态转移的触发器(enable/resolve)和“一次性”的侦听器(wait/then),最后实现了同时依赖多个异步事件(或promise)的语法工具,包括并发事件(when)和有先后顺序的事件流(follow和end)。所以这个模块不是基于自顶向下的设计,而是在逐步的实践、hack和验证中发展出来的,上面提到的各种模式词汇都是“事后美化”,我觉得大多数“设计模式”也是这样——是对实践方法的归纳和描述,而不是在实践中套用的“新技术”。
开始帖使用范例~
把Event实例单独定义为模块,承担应用各模块之间的消息传递:
- oz.def("notify", ["event"], function(Event){
- return Event(); // 以下例子里省略def/require声明,继续沿用notify和Event这两个局部变量名
- });
为基础类生成独立的事件命名空间,不依赖应用级的全局事件:
- Dialog.prototype = {
- init: function(opt){
- this.event = Event();
- },
- update: function() {
- this.updateSize();
- this.updatePosition();
- this.event.fire("update", [this]);
- return this;
- },
监听消息和解除监听:
- notify.bind("msg:A", function(msg){
- a = msg;
- notify.unbind("msg:A", arguments.callee);
- });
发送消息:
- setTimeout(function(){
- notify.fire("msg:A", ["hey jude"]);
- }, 1000);
状态转移:
- $("#button1").click(function(e){
- notify.resolve("button1:clicked", [this]);
- notify.bind("button1:clicked", function(button){
- // 按钮1已经点击过,所以立刻执行
- button.style.color = 'black';
- });
- });
- notify.bind("button1:clicked", function(button){
- // 等待按钮1点击之后再执行
- button.style.color = 'red';
- });
异步回调:
- var data = {
- load: function(url){
- $.getJSON(url, function(json){
- if (json)
- notify.resolve("data:" + url, [json]);
- else
- notify.reject("data:" + url);
- });
- return notify.promise("data:" + url);
- }
- };
- data.load("jsonp_data_1.js").then(function(json){
- // json callback
- }, function(){
- // json error
- });
也可以用自己的promise对象:
- var promise = Event.Promise();
- $.ajax({
- url: "jsonp_data_1.js",
- success: function(json){
- promise.resolve(json);
- promise.fire("json loaded");
- },
- error: function(){
- promise.reject();
- promise.error("json error");
- }
- });
- // fire和error都会执行bind的参数,resolve执行then和bind,所以bind的参数会被执行2次
- // 如果ajax请求在之前已经返回,则只有then或fail的参数会被执行(因为他们监听的是“状态改变”)
- promise.bind(function(){}).then(function(){}).fail(function(){});
事件流:
- notify.promise("data:jsonp_data_1.js").then(function(json){
- setTimeout(function(){
- notify.resolve("delay:1000", [+new Date(), json]);
- }, 1000);
- return notify.promise("delay:1000");
- }).follow().then(function(time, json){
- setTimeout(function(){
- console.log("[数据在3秒前加载成功]", json);
- }, 2000);
- }).end().fail(function(msg){
- return notify.promise("data:error").resolve([msg]);
- }).follow().then(function(){
- console.log("[数据加载失败]", msg);
- });
避免多层的回调嵌套(“callback hell”):
- var fs = require("fs");
- fs.readFile(input, 'utf-8').then(function(err, data){
- var beautifuldata = js_beautify(data, options);
- // 需要修改readFile和writeFile传出promise对象
- return fs.writeFile(output, beautifuldata);
- }).follow().then(function(err){
- if (err)
- throw err;
- console.log('Success!');
- });
依赖多个并发事件:
- notify.when("msg:A", "msg:B", "jsonp:A", "jsonp:B") // when传出新的promise对象
- .some(3) // 如果不调用some或any,默认为全部事件完成后再触发resolve
- .then(function(){ // 已经取到3/4的数据,参数顺序跟when的参数顺序一样
- console.warn("recieve 3/4", arguments);
- });
静态方法Event.when接受promise参数,可以写出更复杂的依赖关系:
- Event.when(
- notify.when("msg:A", "msg:B"),
- notify.when("click:btn1", "clicked:btn2").any()
- ).then(function(args1, args2){
- // 相当于:"msg:A" && "msg:B" && ( "click:btn1" || "clicked:btn2" )
- console.warn("recieve all messages, click one button", arguments);
- });
测试demo:https://github.com/dexteryy/OzJS/blob/master/tests/test_event.html
可以在console里观察执行顺序⋯⋯
从这个测试页可以看出我连单元测试都懒得写⋯⋯