ES6 的异步操作 - Generator 和 async

继续读《ECMAScript 6 入门》。在 ES6 之前,异步编程大概有四种方法:回调函数、事件监听器模式(发布/订阅模式)、流程控制库和 Promise/Deferred 模式。为了使异步操作更加清晰、简洁、方便,ES6 引入了 Generator 函数,而 ES2017 标准引入了 async 函数作为 Generator 函数的语法糖。

由于 JavaScript 是单线程,异步操作就显得格外重要。web 框架 koa 1.0 使用 Generator 实现异步,而基于 ES7 的 koa 2 完全使用 Promise 并配合 async 来实现异步。因此,在着手去了解这些基于 NodeJS 的 web 框架前,我决定先补充关于 ES6 的异步操作的知识。

本博文在 Promise 对象 | 大黄菌的个人博客 之后食用最佳。

:HTML 5 提出了 Web Worker 标准,允许 JS 脚本创建多个线程以充分利用多核 CPU 的计算能力。但子线程不得操作 DOM,且完全受主线程控制,因此此标准并没有改变 JavaScript 单线程的本质。

Generator 函数

Generator.png

Generator 与协程

一个线程(或函数)执行到一半,可以暂停执行,将执行权交给另一个线程(或函数),等到稍后收回执行权的时候,再恢复执行。这种可以并行执行、交换执行权的线程(或函数),就称为协程(coroutine)

Generator 函数是 ES6 对协程的不完全实现,因为只有 Generator 函数的调用者才能将程序的执行权还给 Generator 函数。如果是完全执行的协程,任何函数都可以让暂停的协程继续执行。

Generator 函数的异步应用

Generator 是一个异步操作的容器。想让 Generator 自动执行,即需要当异步操作有结果时能够自动交回执行权。两种方法:

  1. 回调函数。将异步操作包装成 Thunk 函数,在回调函数里交回执行权。
  2. Promise 对象。将异步操作包装成 Promise 对象,用then方法交回执行权。

其中,JavaScript 中的 Thunk 函数指将多参数函数替换成的一个只接受回调函数作为参数的单参数函数。任何参数有回调函数的函数,都能写成 Thunk 函数的形式。

1
2
3
4
5
6
7
8
// ES6 版本的简单 Thunk 函数转换器
const Thunk = function(fn){
return function(...args){
return function(callback){
return fn.call(this, ...args, callback);
}
}
}

前者的实现可用 Thunkify 模块,后者可用 co 模块。了解更多请看 Generator 函数的异步应用 - ECMAScript 6入门。暂时用不到的工具就不进一步消耗脑细胞了。

实例

通过 Generator 函数部署 Ajax 操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function main () {
var result = yield request('http://some.url');
var resp = JSON.parse(result);
console.log(resp.value);
}

function request (url) {
makeAjaxCall(url, (response) => {
it.next(response);
});
}

var it = main();
it.next();

使用 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
function Tree(left, label, right) {
this.left = left;
this.label = label;
this.right = right;
}

// 中序(inorder)遍历函数
function* inorder(t) {
if(t) {
yield* inoreder(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 inoreder(tree)){
result.push(node);
}

result
// ['a', 'b', 'c', 'd', 'e', 'f', 'g']

利用 Generator 函数部署 Iterator 接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function* iterEntries (obj) {
let keys = Object.keys(obj);
for(let i = 0; i < keys.length; i++) {
let key = keys[i];
yield [key, obj[key]];
}
}

let myObj = {
foo: 3,
bar: 7
};

for(let [key, value] of iterEntries(myObj)) {
console.log(key, value);
}

// foo 3
// bar 7

async 函数

async+函数.png

感觉前面的知识短时间内都消化不了,就没有看处于提案的异步遍历器。

实例

异步获取股票报价

1
2
3
4
5
6
7
8
9
async function getStockPriceByName (name) {
var symbol = await getStockStmbol(name);
var stockPrice = await getStockPrice(symbol);
return stockPrice;
}

getStockPriceByName('goog').then((result) => {
console.log(result);
});

并发发出远程请求

1
2
3
4
5
6
7
8
9
10
11
12
async function logInorder(urls) {
// 并发读取远程 URL
const textPromises = urls.map(async url => {
const response = await fetch(url);
return response.text();
});

// 按次序输出
for(const textPromise of textPromises) {
console.log(await textPromise);
}
}

结语

经过学习,我感觉 async 函数真的为一组资源依次异步加载等实际开发场景提供了一种非常简便的处理方案。不过想要运用好 async 函数,深究其实现原理,就必须也要对 Generator 函数有清晰的认知。

异步编程不管是在浏览器还是在服务器端的开发都很重要,知识量也很大,一时半会大概消化不过来。在总结完这些知识后,还需要时常复习,并在实际开发过程中探索更好的实践。

补充阅读

JavaScript 运行机制详解:再谈Event Loop - 阮一峰的网络日志:帮助加深对同步、异步执行机制的认识。