异步编程(二)

笔记部分内容摘自 《深入浅出Node.JS》 —— 朴灵

解决方案

  • 事件发布/订阅模式
  • Promise/Deferred模式
  • 流程控制库

事件发布/订阅模式

常常用来解耦业务逻辑

Node.js内的实现

Node自身提供的events模块,是事件发布/订阅模式的简单实现。
通过模块提供的监听、触发等方法实现。

1
2
3
4
5
6
7
//订阅
emitter.on("event1", function (message) {
//TODO
})

//发布
emitter.emit("event", "message");

事件发布/订阅模式 可以将一个事件与多个回调关联起来。一个事件的发布,可以传递给所以正在侦听此事件的监听器(数量设限,过多可能会导致内存泄漏)执行。

多异步依赖

通过添加哨兵变量偏函数的方式,来解决多异步协作的问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//只有在times个异步都处理完后才能调用callback
var allDone = function (times, callback) {
var count = 0,
results = {};

return function (key, value) {
results[key] = value;
++count;

if (count === times) {
callback(results);
}
}
}

var done = allDone(times, callback);

可使用第三方模块 EventProxy 来解决事件订阅/发布模式的一些问题。
github or npm

Promise/Deferred 模式

Promise 是一种规范,Promise 都拥有一个叫做 then 的唯一接口,当 Promise 失败或成功时,它就会进行回调。
它代表了一种可能会长时间运行而且不一定必须完成的操作结果。这种模式不会阻塞和等待长时间的操作完成,而是返回一个代表了承诺的(promised)结果的对象。
Defferred 就是之后来处理回调的对象。二者紧密不可分割。

Promise对象只要具备then()方法即可,对于then()方法,有以下几点要求:

  • 接受完成态、错误态的回调方法。
  • 可选的支持progress事件回调作为第三个方法。
  • then()方法只接受function对象,其余对象直接被忽略。
  • then()方法须继续返回Promise对象,以实现链式调用。
1
2
//then()方法的定义
then(fulfilledHandler, errorHandler, progressHandler)

Deferred对象(延迟对象)以用来实现触发执行被Promise.then()保存起来的回调函数的地方。

Promise/Deferred模式 对象关系示意图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
function start(){
var d = new Deffered();

offWork(function(){
d.resolve('done----offWork');
})

return d.promise;
}

start().then(function(){
var d = new Deffered();

backHome(function(){
d.resolve('done----backhome');
})

return d.promise;

}).then(function(){
var d = new Deffered();

eatFood(function(){
d.resolve('done----eatFood');
})

return d.promise;

console.log('eating');
})

流程控制库

尾触发与Next

除了事件和Promise外,还有一类方法需要手工调用才能持续执行的后续调用的,我们将此方法叫做尾触发。
常见的关键词是Next。

这种方法常用在Connect中间件中使用。
通过对代码改造,在每个方法后面增加next参数,以连续调用下一个中间件,行成处理流。

中间件机制使得在处理网络请求时,对请求进行过虑、验证、记录日志等功能,与具体业务代码解耦。

使用尾触发会导致任务串行执行,将串行的逻辑扁平化,由此不能很好的发挥出异步的优势。

async

异步的串行执行

Sample:通过series()方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
async.series([
function (callback) {
fs.readFile('file1.txt', 'UTF-8', callback);
},

function (callback) {
fs.readFile('file2.txt', 'UTF-8', callback);
},

function (callback) {
fs.readFile('file3.txt', 'UTF-8', callback);
}
], function (err, results) {
//results => [file1, file2, file3]
})

每个callback会在执行时将结果保存起来,然后执行下一个调用,当全部的调用结束,最后将全部的结果以数组的形式传递给最后的调用。过程中,有任何一个调用出现异常,就直接结束所有调用。

异步的并行执行

Sample:parallel()方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
async.parallel([
function (callback) {
fs.readFile('file1.txt', 'UTF-8', callback);
},

function (callback) {
fs.readFile('file2.txt', 'UTF-8', callback);
},

function (callback) {
fs.readFile('file3.txt', 'UTF-8', callback);
}
], function (err, results) {
//results => [file1, file2, file3]
})

与series()类似,只是parallel()以并行方式执行。

异步调用的依赖处理

Sample:waterfall()方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
async.parallel([
function (callback) {
fs.readFile('file1.txt', 'UTF-8', function (err, content) {
callback(err, content);
});
},

function (callback) {
fs.readFile(args, 'UTF-8', function (err, content) {
callback(err, content);
});
},

function (callback) {
fs.readFile(args, 'UTF-8', function (err, content) {
callback(err, content);
});
}
], function (err, results) {
//results => results4
})

通过这种方法实现异步的依赖处理。异步函数依次执行,将异步方法得出的结果作为参数传递给下一个调用。

其他

详情用法见官方文档

step

串行执行

只有一个接口step,step接受任意数量的任务,所有任务一次串行执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Step(
function readFile1() {
fs.readFile('file1', 'utf8', this);
},
function readFile2(err, content) {
fs.readFile('file2', 'utf8', this);
},
function readFile3(err, content) {
fs.readFile('file3', 'utf8', this);
},
function done(err, results) {
console.log(results);
}
)

在Step中用到了关键字this,他是Step内部的next()方法,将异步调用的结果传递给下一个任务作为参数并执行。

并行执行

使用step中的parallel()方法

1
2
3
4
5
6
7
8
9
Step(
function readFiles() {
fs.readFile('file1', 'utf8', this.parallel());
fs.readFile('file2', 'utf8', this.parallel());
},
function done(err, res1, res2) {
console.log(arguments);
}
)

并行执行,按异步调用结束的顺序,一次返回调用的结果。

值得注意的是,如果异步方法的结果传回的是多个参数,Step()将只会取前两个参数

wind

看了几个博客,都是一样的内容,目前还不太理解,暂时直接摘录的别人的博客内容。

摘自:purplebamboo’s blog

还有种比较知名的方式,是国内的程序员老赵的 wind.js, 它使用了一种完全不同的异步实现方式。前面的所有方式都要改变我们正常的编程习惯,但是 wind.js 不用。它提供了一些服务函数使得我们可以按照正常的思维去编程。

下面是一个简单的冒泡排序的算法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var compare = function (x, y) {
return x - y;
}

var swap = function (a, i, j) {
var t = a[i]; a[i] = a[j]; a[j] = t;
}

var bubbleSort = function (array) {
for (var i = 0; i < array.length; i++) {
for (var j = 0; j < array.length - i - 1; j++) {
if (compare(array[j], array[j + 1]) > 0) {
swap(array, j, j + 1);
}
}
}
}

很简单就不讲解了,现在的问题是我们如果要做一个动画,一点点的展示这个过程呢。
于是我们需要给 compare 加个延时,并且 swap 后重绘数字展现。
可 javascript 是不支持 sleep 这样的休眠方法的。如果我们用 setTimeout 模拟,又不能保证比较的顺序的正确执行。

可是有了 windjs 后我们就可以这么写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var compareAsync = eval(Wind.compile("async", function (x, y) {
$await(Wind.Async.sleep(10)); // 暂停10毫秒
return x - y;
}));

var swapAsync = eval(Wind.compile("async", function (a, i, j) {
$await(Wind.Async.sleep(20)); // 暂停20毫秒
var t = a[i]; a[i] = a[j]; a[j] = t;
paint(a); // 重绘数组
}));

var bubbleSortAsync = eval(Wind.compile("async", function (array) {
for (var i = 0; i < array.length; i++) {
for (var j = 0; j < array.length - i - 1; j++) {
// 异步比较元素
var r = $await(compareAsync(array[j], array[j + 1]));
// 异步交换元素
if (r > 0) $await(swapAsync(array, j, j + 1));
}
}
}));

注意其中最终要的几个辅助函数:

  1. eval(Wind.compile(“async”, func) 这个函数用来定义一个 “异步函数”。这样的函数定义方式是 “模板代码”,没有任何变化,可以认做是 “异步函数” 与 “普通函数” 的区别。
  2. Wind.Async.sleep() 这是 windjs 对于 settimeout 的一个封装,就是用上面的 eval(Wind.compile 来定义的。
  3. $await() 所有经过定义的异步函数,都可以使用这个方法 来等待异步函数的执行完毕。

这样上面的代码就可以很容易的理解了。compare,swap 都被弄成了异步函数,然后使用 $await 等待他们的执行完毕。可以看到跟我们之前的写法比起来,实现思路几乎一样,只是多了些辅助函数。相当的创新。

windjs 的实现原理,暂时没怎么看,这是一种预编译的思路。之后有空看看也来实现一个简单的 demo。


无聊小记

想起来以前看到别人写过的一个排序算法 —— sleepSort 挺搞笑的。

1
2
3
4
5
var arr = [2, 4, 13, 12, 34, 11, 42, 23, 6, 32, 23];

arr.map(num => {
setTimeout(() => console.log(num), num);
});

generator

直接看代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function* fib() {
var a = 0;
var b = 1;
while(true) {
yield a;
[a, b] = [b, a + b];
}
}

//one method
let [first, second, third, fourth, fifth, sixth] = fib();
console.log(third); //1
console.log(sixth); //5

//the onther
let it = fib();
console.log(it.next()); //object {value: '0', done: false}
console.log(it.next()); //object {value: '1', done: false}
console.log(it.next()); //object {value: '1', done: false}
console.log(it.next()); //object {value: '2', done: false}
console.log(it.next()); //object {value: '3', done: false}
console.log(it.next()); //object {value: '5', done: false}
console.log(it.next()); //object {value: '8', done: false}

function* funcName(...params){...} 生成器函数 function generator

不多说,详见MDN官方文档

小结

异步编程,在性能问题上尤其是 io 处理上是它的优势,但是同时也是它的劣势,大部分人都无法很好的组织异步代码。于是就出现了一大堆的库,来给它擦屁股。


世界上本没有嵌套回调,写的人多了,也便有了。