JS异步

promise

解决了“回调地狱”的问题,使异步回调的代码书写由横向变为纵向,但是一堆then的语义性不够好。

协程

yield命令。执行到该命令时该协程的执行权交给另一个协程,一段时间后执行权交回时才继续执行。

优点是写法很像同步操作的代码。

Generator函数

ruanyf - Generator函数

协程在ES6中的实现就是Generator函数。

不同于普通函数的地方:

  • 函数可以暂停执行,用yield命令。
  • 调用Generator函数会返回内部指针对象(遍历器),其具有的next方法可以控制Generator函数的继续执行。
1
2
3
4
5
6
7
8
9
10
11
12
13
function* gen(x){
var y = yield x + 2;
return y;
}
var g = gen(1);
console.log(g); //{}
var g1 = g.next();
console.log(g1); //{ value: 3, done: false }
var g2 = g.next();
console.log(g2); //{ value: undefined, done: true }

这里有个小注意点,valueundefined的前一步next执行结果中,done一定为false吗?看下面这个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function* helloWorldGenerator() {
yield 'hello';
yield 'world';
yield 'ending'; //这里使用yield
}
var hw = helloWorldGenerator();
console.log(hw.next());
// { value: 'hello', done: false }
console.log(hw.next());
// { value: 'world', done: false }
console.log(hw.next());
// { value: 'ending', done: false } 注意这里是false
console.log(hw.next());
// { value: undefined, done: true }

如果把上例中的yield 'ending'改成return 'ending',则下面第三个next方法执行后会返回{ value: 'ending', done: true }

也就是说,如果Generator函数最后是return语句,则执行完return语句后,遍历器的done就是true了;如果没有return语句,则执行完最后一步yield命令的表达式之后,遍历器的done会是false,需要再执行一次next才会变为false.

next方法传的参数,会作为上个阶段异步任务的返回结果,被函数体内return的变量接收。因此,传参执行next返回的遍历器的value就是这个返回的变量,也即传入的参数?那传参的意义是什么?

Generator函数内部也可以用try...catch捕获函数体外抛出的错误,在函数体外抛出错误g.throw('error occurred!'),在函数体内捕获错误进行处理,实现异步编程中出错代码与处理错误的代码的时间、空间上的分离。

Generator函数的自动执行

Generator 函数就是一个异步操作的容器。它的自动执行需要一种机制,当异步操作有了结果,能够自动交回执行权。两种方法可以做到这一点:

(1)回调函数。将异步操作包装成 Thunk 函数,在回调函数里面交回执行权。

(2)Promise 对象。将异步操作包装成 Promise 对象,用 then 方法交回执行权。

用Thunk函数实现Generator函数的自动执行

ruanyf - Thunk函数

传值调用:传入计算好的参数值

传名调用:传入参数表达式,函数体内只在需要参数值的时候才计算该表达式。

Thunk函数是传名调用的实现,将参数表达式放在临时函数中,将这个临时函数名传入。需要时执行这个临时函数。该临时函数就是Thunk函数。

JavaScript中的Thunk函数:将多参数函数(参数中必须含有回调函数),替换成单参数版本,切只接受回调函数作为参数。

生产环境使用Thunkify模块作为转换器。Thunkify源码

Thunk函数可用于Generator函数的自动流程管理:在回调函数中将执行权交还给Genenrator函数。

手动执行:

1
2
3
4
5
6
7
8
9
10
11
var g = gen();
var r1 = g.next();
r1.value(function(err, data){
if (err) throw err;
var r2 = g.next(data);
r2.value(function(err, data){
if (err) throw err;
g.next(data);
});
});

用Thunk函数自动执行:Generator函数中的yield命令后面必须是Thunk函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var gen = function* (){
var f1 = yield readFile('fileA');
var f2 = yield readFile('fileB');
// ...
var fn = yield readFile('fileN');
};
function run(fn) {
var gen = fn();
function next(err, data) {
var result = gen.next(data);
if (result.done) return;
result.value(next);
}
next();
}
run(gen);

用Promise实现Generator函数的自动执行

ruanyf - co

co函数库返回一个Promise对象。Generator 函数的 yield 命令后面,只能是 Thunk 函数或 Promise 对象。

1
2
3
4
5
6
7
8
9
var gen = function* (){
var f1 = yield readFile('/etc/fstab');
var f2 = yield readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};
var co = require('co');
co(gen);

co源码

主要是递归执行gen.next(),每一步执行后返回的值的value(ret.value)都用一个promise包裹起来,然后在这个promise的then函数中的回调函数中继续执行gen.next()

co支持并发的异步操作,用数组或对象存储这些兵法操作,等它们全部完成之后才进行下一步。

async函数

async函数

“异步编程的最高境界,就是根本不用关心它是不是异步。”

async函数其实是Generator函数的语法糖。async 函数就是将 Generator 函数的星号(*)替换成 async,将 yield 替换成 await,仅此而已。

与Generator函数相比,async函数有如下优点:

  • 内置执行器:Generator函数的执行必须依靠执行器。
  • 更好的语义:async - 函数内部有异步操作,await - 紧跟在后面的表达式需要等待结果。
  • 更广的适用性:Genertor函数使用自动执行器时,yield后面要跟Thunk函数或者Promise对象;await后面可以跟Promise对象(一般是个函数,执行该函数返回的Promise对象)或原始类型值(后者等于同步操作)。

async函数返回一个Promise对象。

一般用try...catchawait包裹起来。

await命令只能用在async函数中。

(1)多个请求并发执行:不能用forEach对每个请求await,而应使用Promise.all

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
async function dbFuc(db) {
let docs = [{}, {}, {}];
let promises = docs.map((doc) => db.post(doc));
let results = await Promise.all(promises);
console.log(results);
}
// 或者使用下面的写法
async function dbFuc(db) {
let docs = [{}, {}, {}];
let promises = docs.map((doc) => db.post(doc));
let results = [];
for (let promise of promises) {
results.push(await promise);
}
console.log(results);
}

(2)多个请求继发执行:用for循环:

1
2
3
4
5
6
7
async function dbFuc(db) {
let docs = [{}, {}, {}];
for (let doc of docs) {
await db.post(doc);
}
}

补充Generator函数语法

next方法的参数表示上一个yield表达式的值。也即:next()是将yield表达式替换成一个值。

第一次调用next方法时不用传参。传了也会被V8引擎忽略。

使用for...of语句时不需要next方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
function *foo() {
yield 1;
yield 2;
yield 3;
yield 4;
yield 5;
return 6;
}
for (let v of foo()) {
console.log(v);
}
// 1 2 3 4 5
1
2
3
4
5
6
7
8
9
10
11
12
function* fibonacci() {
let [prev, curr] = [0, 1];
for (;;) {
[prev, curr] = [curr, prev + curr];
yield curr;
}
}
for (let n of fibonacci()) {
if (n > 1000) break;
console.log(n);
}

next(), throw(), return()都是对yield语句后面表达式的替换。

yield*

yield*后面的 Generator 函数(没有return语句时),不过是for...of的一种简写形式,完全可以用后者替代前者。反之,在有return语句时,则需要用var value = yield* iterator的形式获取return语句的值。

yield*取出嵌套数组的所有成员:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function* iterTree(tree) {
if (Array.isArray(tree)) {
for(let i=0; i < tree.length; i++) {
yield* iterTree(tree[i]);
}
} else {
yield tree;
}
}
const tree = [ 'a', ['b', 'c'], ['d', 'e'] ];
for(let x of iterTree(tree)) {
console.log(x);
}
// a
// b
// c
// d
// e

yield*遍历完全二叉树:

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
33
34
35
// 下面是二叉树的构造函数,
// 三个参数分别是左树、当前节点和右树
function Tree(left, label, right) {
this.left = left;
this.label = label;
this.right = right;
}
// 下面是中序(inorder)遍历函数。
// 由于返回的是一个遍历器,所以要用generator函数。
// 函数体内采用递归算法,所以左树和右树要用yield*遍历
function* inorder(t) {
if (t) {
yield* inorder(t.left);
yield t.label;
yield* inorder(t.right);
}
}
// 下面生成二叉树
function make(array) {
// 判断是否为叶节点
if (array.length == 1) return new Tree(null, array[0], null);
return new Tree(make(array[0]), array[1], make(array[2]));
}
let tree = make([[['a'], 'b', ['c']], 'd', [['e'], 'f', ['g']]]);
// 遍历二叉树
var result = [];
for (let node of inorder(tree)) {
result.push(node);
}
result
// ['a', 'b', 'c', 'd', 'e', 'f', 'g']

Generator与状态机

1
2
3
4
5
6
7
8
9
10
11
12
var ticking = true;
var clock = function() {
if (ticking)
console.log('Tick!');
else
console.log('Tock!');
ticking = !ticking;
}
clock(); //Tick!
clock(); //Tock!
clock(); //Tick!
clock(); //Tock!

用Generator函数改写如下:不需要用外部变量来保存状态(因为),更安全。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var clock = function* () {
while (true) {
console.log('Tick!');
yield;
console.log('Tock!');
yield;
}
};
var g = clock();
g.next(); //Tick!
g.next(); //Tock!
g.next(); //Tick!
g.next(); //Tock!
分享
0%