JavaScript 异步编程

异步,顾名思义,就是不同的,不连续的步骤。简单说就是一个任务分两步,第一步执行完,等待一些时间才执行第二步。常见于网络请求、文件读取等操作。

在 JavaScript 中,解决异步的方案主要有回调、Promise、Generator、Async 几种,下面分别举例示范。

回调

JavaScript 异步编程的默认实现是使用回调函数,也就是将第二步的操作写在一个函数里,在第一步执行完之后,这个函数开始执行。看一个 Node 官方 API 的栗子:

1
2
3
4
fs.readFile('/etc/passwd', 'utf8', (err, data) => {
if (err) throw err;
console.log(data);
});

如果只是这种简单的异步没问题,但在多层异步的情况下,比如,先获取一个个 url 的内容,然后再获取下一个,为了保证获取顺序的正确,必须写成多重嵌套,最终导致 callback hell 。

1
2
3
4
5
6
7
fs.readFile('xxx', (err, data) => {
fs.readFile('yyy', (err, data) => {
fs.readFile('zzz', (err, data) => {
...
}
}
});

Promise

Promise 是抽象异步处理对象并对其进行各种操作的组件,在 ES6 里可直接使用。下面用 Promise 对前面的异步行为重写:

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
function read(fileName) {
return new Promise((resolve, reject) => {
fs.readFile(fileName, 'utf8', (err, data) => {
if (err) {
reject(err);
}
resolve(data);
})
})
}

read('/etc/passwd')
.then(data => console.log(data));

// 串行读取多个
read('xxx')
.then(data => {
console.log(data);
return read('yyy');
})
.then(data => console.log(data));

// 并行读取多个
Promise.all([read('xxx'), read('yyy')])
.then(values => {
console.log(values[0]);
console.log(values[1]);
})

可以看到,这样避免了代码的横向发展,而且使得异步两段的执行更清晰,但产生了代码冗余,原来的异步任务都需要用 Promise 包一下。

更多关于 Promise 用法的介绍请看:Promise 迷你书

Generator

Generator 是协程在 ES6 的实现,它不同于普通函数,是可以暂停执行的,所以在函数名前加 * 以区别普通函数。整个 Generator 函数是一个封装的异步任务,需要暂停的异步操作前面用 yield 注明。遇到 yield 就暂停,等到执行权返回,再从暂停的地方往后执行,yield 是异步两个阶段的分界线。

1
2
3
4
5
function* gen(x) {
yield x = x * 2;
yield y = x + 1;
return y;
}

调用 Generator 函数会返回一个内部指针,调用指针的 next() 方法,会移动内部指针到 yield 语句。next() 方法的作用是分阶段执行 Generator 函数,每次执行返回一个包含 value(yield 语句后面表达式的值)和 done(Generator 是否执行完毕)的对象。

1
2
3
4
let g = gen(2);
console.log(g.next()); // { value: 4, done: false }
console.log(g.next()); // { value: 5, done: false }
console.log(g.next()); // { value: 5, done: true }

下面是一个用 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
function* request(url) {
yield fetch('https://api.github.com');
yield fetch('https://api.github.com/users/huangtengfei');
}

let req = request();
let apis = req.next();
let user = req.next();

apis.value
.then(res => res.json())
.then(data => console.log(data));

user.value
.then(res => res.json())
.then(data => console.log(data));

// 并行执行多个任务
function* parallelTasks() {
let [resultA, resultB] = yield [
taskA(),
taskB()
];
console.log(resultA, resultB);
}

使用 Generator 写异步操作很简洁,就像写同步操作一样,但是流程管理不方便,要手动执行异步的不同阶段。

co

Generator 是一个异步执行的容器,它的自动执行需要一种机制:当异步操作有了结果,能够自动交回控制权。将异步操作包装成 Thunk 函数(在回调函数中交回)或 Promise 对象(在 then 中交回),可以做到这一点。

co 正是对这两种机制的包装,因此使用 co 要求 Generator 的 yield 后面必须是 Thunk 函数或 Promise 对象。

看个 co 官方的栗子:

1
2
3
4
5
6
7
8
9
10
const co = require('co');

co(function* () {
var result = yield Promise.resolve(true);
return result;
}).then(value => {
console.log(value);
}, err => {
console.log(err.stack);
});

使用 co 写异步很简洁,但缺点也正是前面提的,yield 后面必须返回一个 Thunk 或 Promise。

Async/Await

async/await 是一个尚在 stage3 的方案,但 Babel 已经 支持转换。它相当于是 Generator 的一种语法糖,将 * 替换成 async ,将 yield 替换成 await。另外,async/await 自带执行器,且不像 co 约定 yield 后面必须跟 Thunk 或 Promise,可以跟 Promise 对象或原始类型的值(此时等同于同步)。

下面是一个使用 async/await 写异步操作的栗子:

1
2
3
4
5
6
7
8
9
10
11
12
function timeout(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}

async function asyncPrint(value, ms) {
await timeout(ms);
console.log(value); // 3s 后输出 hello world
}

asyncPrint('hello world', 3000);

参考

异步操作和Async函数 - 阮一峰

坚持原创技术分享,您的支持将鼓励我继续创作!