JavaScript异步解决方案-generator

前言

在文章 把书看薄-深入浅出nodejs-异步控制 中,我们探讨了几种传统的异步控制解决方案,随着ES6的普及,有了新的异步解决方案 - generator。

基础

Iterator

Iterator即迭代器。是ES6规范中的一种新的数据结构。Iterator对象有一个属性方法 next ,每次调用next的时候返回一个状态:{done: false/true, value: xxx},当状态完成之后,将done置为true。

generator

generator会自动返回一个Iterator对象,其语法表示为 function * ,当generator函数执行之后,可以对返回的Iterator进行迭代,即不断的调用next方法,直到状态转换完毕。

yield

yield必须存在于generator函数体里面,当generator返回的Iterator对象调用next方法时,正常的执行流遇到yield关键字时候就会停止,退出函数,返回状态为false,返回值赋值为yield的返回值,同时保存现场。下次调用next方法的时候会恢复现场,从退出的语句开始继续执行。如果遇到return语句或者正常函数执行完毕,返回状态为true,返回值对应return的值。

yield*

yield后面可以跟一个Iterator对象,写作 yield *xxxIter。此时会形成一个类似嵌套的效果,调用next方法时会进入xxxIter,直到把xxxIter中的状态全部走完。

next

Iterator对象具有next方法,每次调用时返回可返回一个状态。同时在调用next方法时可以传递一个参数到next方法中,传递的参数将会被赋值给 yield 的返回值(默认yield 语句是没有返回值的),此处如此设计就是为了让next方法的调用方可以和generator内部通信,解决异步问题的关键就在于这个特性,我们后面会详细说明。

示例代码

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
31
32
//test为generator函数
function* test() {
console.log('in'); --- (1)
var testRt = yield 1; --- (2) //测试普通yield
console.log(testRt); --- (3)
var testRt2 = yield 2;--- (4) //测试next传递参数
console.log(testRt2); --- (5)
yield *subTest(); --- (6) //测试yield*
return 4;
}
function *subTest() {
console.log('sub in'); --- (7)
yield 3; --- (8)//测试嵌套generator
}
var testIter = test(); //返回Iterator对象,具有next方法可迭代
//1 第一次调用next方法
testIter.next();
//执行(1) (2)
//输出 in
//返回 {'done': false, value: 1}
//2 第二次调用next方法
testIter.next('passArg');
//执行(3) (4)
//输出 passArg ,说明我们传递的参数被赋值给了testRt
//返回 {'done': false, value: 2}
testIter.next();
//执行(5)(6)(7)(8)
//(5)输出 undefined ,说明 yield默认是无返回值的
//(7)输出 sub in,说明进入了子generator函数,并一直执行直到遇到yield语句
//返回 {'done': false, value: 3}
testIter.next();
//返回 {'done': true, value: 4},执行完毕

利用generator解决异步问题

从上面的基础我们了解到,generator函数可以实现执行到yield之后保存当前运行环境并退出,直到下一次next方法继续执行。这里我们似乎可以看到利用点,利用两次 next 方法调用之间来执行异步函数,并且在异步执行完之后调用下一次next方法

场景:

1
2
3
4
5
6
//回调嵌套写法
$.get(url1, function(url2) {
$.get(url2, function(data) {
console.log('done');
});
});

我们预想可以通过generator变成同步的书写方式:

1
2
3
4
5
function* work() {
var url2 = yield request(url1);
var data = yield request(url2);
console.log('done');
}

思路分解

  • 1.想要上述代码正常执行,第一步肯定是对work初始化,获取Iterator:
  • 1
    var workIter = work();
  • 2.利用generator的特点,我们开始执行next,第一步应该是执行get url1,同时代码退出,直到url1返回,调用下一次next。

    要实现url1返回之后才能调用下一次next,所以上一次next返回来的数据应该是一个异步方法。这样我们就可以在其callback中放置下一次的next,同时可以将第一次的返回url2通过next的参数传递进generator函数内部,在执行下一次next时使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var request1 = workIter.next();
//此时应该执行语句 var url2 = yield request(url1)
//返回 {'done': false, value: request(url1)}
if (!request1.done) { //异步流程还没有结束
var syncFn = request1.value; //value 即 request(url1)的返回值,是一个异步fn
syncFn(function(e, data) {
if (!e) {
var request2 = workIter.next(data);
//request2 也是一个异步方法
if (!request2.done) {
var syncFn = request2.value;
syncFn(function(e, data) {
...
});
}
}
})
}

整合方法

从上面的思路可以看出,虽然generator work 方法写起来是同步的,但是执行其内部next的方法需要处理多次回调,层层嵌套,不可能手动去触发next方法直到结束,所以我们需要一个方法来自动触发next方法,直到结束。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function geHelper(worker) {
var workerIter = worker();
var next = function (data) {
var result = workerIter.next(data);
if (!result.done) {
result.value(function (err, data) {
if (err) {
throw err;
}
next(data);
});
}
};
next();
}

同时,我们需要封装request方法,让其返回一个异步fn。

1
2
3
4
5
function request(url) {
return function(cb) {
$.get(url, cb)
}
}

现在可以使用上述的简单封装方法来完成需求了:

1
2
3
4
5
geHelper(function*() {
var url2 = yield request(url1);
var data = yield request(url2);
console.log('done');
});

co

上述封装还是太过简陋,业内早有成熟的方案:co

co大致思路和我们的一样,不过其将返回结果promise化,更符合未来异步的潮流。简单使用方法:

1
2
3
4
5
6
7
8
co(function* () {
var result = yield Promise.resolve(true);
return result;
}).then(function (value) {
console.log(value);
}, function (err) {
console.error(err.stack);
});

co的generator函数中,yield后面可以兼容的几种写法为:

  • promise,即yield返回一个promise,co可在内部自动控制在then方法里面置入下一次next方法。
  • Thunks, 即我们上述request类似函数,返回一个只有一个参数即为callback的异步函数。
  • array,可以保护多个promise/thunks
  • Generators,即function*,嵌套generator
喝杯咖啡,交个朋友