回调函数
这是异步编程中最最基本的方法。
假如有两个函数f1和f2,后者等待前者的执行结果
f1();
f2();
如果f1是个很耗时的任务,可以考虑改写f1, 把f2, 写成f1的回调函数
function f1(callback) {
setTimeout(function(){
// f1的任务代码
callback();
}, 1000);
}
执行代码就变成了下面这样:
f1(f2);
回调函数的优点是简单、容易理解和部署,缺点是不利于代码的阅读和维护,各个部分之间高度耦合)(Coupling),流程会很混乱,而且每个任务只能指定一个回调函数。
事件监听
另外一种思路就是采用事件驱动模式。任务的执行不取决于代码的顺序,而是取决于某个事件是否发生。
还是以f1和f2为例。首先,为f1绑定一个事件(这里采用的jQuery的写法)
f1.on('done', f2);
上面这行代码的意思是,当f1发生done事件,就执行f2。然后,对f1进行改写:
function f1(){
setTimeout(function () {
// f1的任务代码
f1.trigger('done');
}, 1000);
}
f1.trigger(‘done’)表示,执行完成后,立即触发done事件,从而开始执行f2。
这种方法的优点是比较容易理解,可以绑定多个事件,每个事件可以指定多个回调函数,而且可以“去耦合”(Decoupling),有利于实现模块化。缺点是整个程序都要变成事件驱动型,运行流程会变得很不清晰。
发布 / 订阅模式
上一节的”事件”,完全可以理解成”信号”。
我们假定,存在一个”信号中心”,某个任务执行完成,就向信号中心”发布”(publish)一个信号,其他任务可以向信号中心”订阅”(subscribe)这个信号,从而知道什么时候自己可以开始执行。这就叫做“发布/订阅模式”(publish-subscribe pattern),又称“观察者模式”(observer pattern)。
这个模式有多种实现,下面采用的是Ben Alman的Tiny Pub/Sub,这是jQuery的一个插件。
首先,f2向”信号中心”jQuery订阅”done”信号。
jQuery.subscribe("done", f2);
然后,f1进行如下改写:
function f1(){
setTimeout(function () {
// f1的任务代码
jQuery.publish("done");
}, 1000);
}
jQuery.publish(“done”)的意思是,f1执行完成后,向”信号中心”jQuery发布”done”信号,从而引发f2的执行。
此外,f2完成执行后,也可以取消订阅(unsubscribe)
jQuery.unsubscribe("done", f2);
这种方法的性质与”事件监听”类似,但是明显优于后者。因为我们可以通过查看”消息中心”,了解存在多少信号、每个信号有多少订阅者,从而监控程序的运行。
Promises对象
promise意为承诺,拟定一个承诺,当承诺实现时即返回结果,不受其他操作的影响,可以把它理解为一个简单的容器,里面存放着一个将来会结束的事件返回结果(即异步操作)。不同于传统的回调函数,在promise中,所有的异步操作的结果都可以通过统一的方法处理。promise有三种状态: pending(进行中),resolved(成功),rejected(失败), 异步操作的结果决定了当前为哪一种状态,promise的状态只有两种改变情况,且仅改变一次:由pending转变为resolved,由pending转变为rejected,结果将会保持不变。
Promises对象是CommonJS工作组提出的一种规范,目的是为异步编程提供统一接口。
简单说,它的思想是,每一个异步任务返回一个Promise对象,该对象有一个then方法,允许指定回调函数。比如,f1的回调函数f2,可以写成:
f1().then(f2);
f1要进行如下改写(这里使用的是jQuery的实现)
function f1(){
var dfd = $.Deferred();
setTimeout(function () {
// f1的任务代码
dfd.resolve();
}, 500);
return dfd.promise;
}
这样写的优点在于,回调函数变成了链式写法,程序的流程可以看得很清楚,而且有一整套的配套方法,可以实现许多强大的功能。
比如,指定多个回调函数:
f1().then(f2).then(f3);
再比如,指定发生错误时的回调函数:
f1().then(f2).fail(f3);
而且,它还有一个前面三种方法都没有的好处:如果一个任务已经完成,再添加回调函数,该回调函数会立即执行。所以,你不用担心是否错过了某个事件或信号。这种方法的缺点就是编写和理解,都相对比较难。
具体使用如下:
- Promise 构造函数只有一个参数,是一个函数,这个函数在构造之后会直接被异步执行,称为起始函数,起始函数有两个参数resolve 和 reject
当Promise被构造时,起始函数会被异步执行
new Promise(function(resolve, reject){
console.log("Run");
});
这段程序会直接输出Run
resolve和reject都是函数,其中resolve代表一切正常,reject是出现异常时调用的
new Promise(function (resolve,reject) {
var a = 0;
var b = 1;
if (b == 0) reject("Diveide zero");
else resolve(a / b);
}).then(function (value) {
console.log("a / b = " + value);
}).catch(function (err) {
console.log(err);
}).finally(function () {
console.log("End");
});
// a / b =0
// End
Promise 类有 .then() .catch() 和 .finally() 三个方法,这三个方法的参数都是一个函数
- .then() 可以将参数中的函数添加到当前 Promise 的正常执行序列
- .catch() 则是设定 Promise 的异常处理序列
- .finally() 是在 Promise 执行的最后一定会执行的序列
.then() 传入的函数会按顺序依次执行,有任何异常都会直接跳到 catch 序列
new Promise(function (resolve,reject) {
console.log(111);
resolve(222);
}).then(function (value) {
console.log(value);
return 333;
}).then(function (value) {
console.log(value);
throw "An error";
}).catch(function (err) {
console.log(err);
});
// 111
// 222
// 333
// An error
resolve() 中可以放置一个参数用于向下一个 then 传递一个值,then 中的函数也可以返回一个值传递给 then
- 但是,如果 then 中返回的是一个 Promise 对象,那么下一个 then 将相当于对这个返回的 Promise 进行操作
- reject() 参数中一般会传递一个异常给之后的 catch 函数用于处理异常
但是请注意以下两点:
- resolve 和 reject 的作用域只有起始函数,不包括 then 以及其他序列
- resolve 和 reject 并不能够使起始函数停止运行,别忘了 return
上述的 “计时器” 程序看上去比函数瀑布还要长,所以我们可以将它的核心部分写成一个 Promise 函数:
function print(delay,message) {
return new Promise(function (resolve,reject) {
setTimeout(function () {
console.log(message);
resolve();
},delay);
});
}
然后我们就可以放心大胆的实现程序功能了:
print(1000,"First").then(function () {
return print(4000,"Second");
}).then(function () {
print(3000,"Third");
});
这种返回值为一个 Promise 对象的函数称作 Promise 函数,它常常用于开发基于异步操作的库
观察者模式和发布订阅
观察者模式
观察者模式定义了对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知,并自动更新。观察者模式属于行为型模式,行为型模式关注的是对象之间的通讯,观察者模式就是观察者和被观察者之间的通讯。
观察者模式有一个别名叫“发布-订阅模式”,或者说是“订阅-发布模式”,订阅者和订阅目标是联系在一起的,当订阅目标发生改变时,逐个通知订阅者。我们可以用报纸期刊的订阅来形象的说明,当你订阅了一份报纸,每天都会有一份最新的报纸送到你手上,有多少人订阅报纸,报社就会发多少份报纸,报社和订报纸的客户就是上面文章开头所说的“一对多”的依赖关系。
订阅-发布模式
其实24种基本的设计模式中并没有发布订阅模式,上面也说了,他只是观察者模式的一个别称。但是经过时间的沉淀,似乎他已经强大了起来,已经独立于观察者模式,成为另外一种不同的设计模式。
在现在的发布订阅模式中,称为发布者的消息发送者不会将消息直接发送给订阅者,这意味着发布者和订阅者不知道彼此的存在。在发布者和订阅者之间存在第三个组件,称为调度中心或事件通道,它维持着发布者和订阅者之间的联系,过滤所有发布者传入的消息并相应地分发它们给订阅者。
举一个例子,你在微博上关注了A,同时其他很多人也关注了A,那么当A发布动态的时候,微博就会为你们推送这条动态。A就是发布者,你是订阅者,微博就是调度中心,你和A是没有直接的消息往来的,全是通过微博来协调的(你的关注,A的发布动态)。
可以看出,发布订阅模式相比观察者模式多了个事件通道,事件通道作为调度中心,管理事件的订阅和发布工作,彻底隔绝了订阅者和发布者的依赖关系。即订阅者在订阅事件的时候,只关注事件本身,而不关心谁会发布这个事件;发布者在发布事件的时候,只关注事件本身,而不关心谁订阅了这个事件。
观察者模式有两个重要的角色,即目标和观察者。在目标和观察者之间是没有事件通道的。一方面,观察者要想订阅目标事件,由于没有事件通道,因此必须将自己添加到目标(Subject) 中进行管理;另一方面,目标在触发事件的时候,也无法将通知操作(notify) 委托给事件通道,因此只能亲自去通知所有的观察者。
发布订阅代码实现
class PubSub {
constructor() {
this.subscribers = [];
}
subscribe(topic, callback) {
let callbacks = this.subscribers[topic];
if(!callbacks) {
this.subscribers[topic] = [callback];
} else{
callbacks.push(callback);
}
}
publish(topic, ...args) {
let callbacks = this.subscribers[topic] || [];
callbacks.forEach(callback => callback(...args));
}
}
// 创建事件调度中心,为订阅者和发布者提供调度服务
let pubSub = new PubSub();
// A订阅了SMS事件(A只关注SMS本身,而不关心谁发布这个事件)
pubSub.subscribe('SMS', console.log);
// B订阅了SMS事件
pubSub.subscribe('SMS', console.log);
// C发布了SMS事件(C只关注SMS本身,不关心谁订阅了这个事件)
pubSub.publish('SMS', 'I published `SMS` event');
观察者实现代码
class Subject {
constructor() {
this.observers = [];
}
add(observer) {
this.observers.push(observer);
}
notify(...args) {
this.observers.forEach(observer => observer.update(...args));
}
}
class Observer {
update(...args) {
console.log(...args);
}
}
// 创建观察者ob1
let ob1 = new Observer();
// 创建观察者ob2
let ob2 = new Observer();
// 创建目标sub
let sub = new Subject();
// 目标sub添加观察者ob1 (目标和观察者建立了依赖关系)
sub.add(ob1);
// 目标sub添加观察者ob2
sub.add(ob2);
// 目标sub触发SMS事件(目标主动通知观察者)
sub.notify('I fired `SMS` event');