js中的异步处理:你想知道的都在这了

  • Kevin
  • 25 Minutes
  • 2017年9月6日

假如你已经知道了什么是异步,并且已经写过很多的异步代码。这篇文章主要介绍几种对异步代码的处理,即异步编码姿势:

  1. 回调函数;
  2. Promise;
  3. 迭代器、生成器;
  4. async/await。

重点在第3、4部分。

回调函数

这个没什么好说的,直接看一段代码:

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

fs.readFile('config.json', (err, data) => {
if (err) {
console.error(err);
} else {
console.log(data);
}
});

后面部分都以该读取文件操作为例来讲解。

Promise

Promise就是为异步而生的,主要是为了解决所谓的回调地狱问题。Promise的三个状态:pendingfulfilledrejected

通常的写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const fs = require('fs');

const promise = new promise((resolve, reject) => {
fs.readFile('config.json', (err, data) => {
if (err) {
reject(err);
} else {
resolve(data);
}
});
});

promise.then(data => {
console.log(data);
}).catch(err => {
console.error(err);
});

需要注意一点的是:new Promise()传入的函数会立即执行,thencatch中传入的函数才是异步执行的。

then方法何时执行?取决于两点:

  1. promise何时变成完成状态(fulfilled);
  2. 在异步队列中的位置。

迭代器、生成器

概念的理解

先理解两个概念:生成器是一个返回迭代器的函数;那么迭代器就是生成器执行后返回的结果(对象)。所以,生成器是函数,迭代器是对象(很容易弄混的两个概念)。

首先,生成器是一个函数,这是一个特殊的函数,函数定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 这就是一个生成器(函数)
function *createIterator() {
const a = yield 1;
const b = yield a + 2;
yield b + 3;
}

// 这就是一个迭代器(对象)
const iterator = createIterator();

// 注释部分是next方法执行的返回值
iterator.next(5); // {value: 1, done: false} 执行完这句并没有给a赋值
iterator.next(1); // {value: 3, done: false} 执行这句的时候才会给a赋值1,(next传进去的值)
iterator.next(5); // {value: 8, done: false} 执行这句的时候才会给b赋值5
iterator.next(); // {value: undefined, done: true}

yield 返回值取决于 next 方法传进去的值,不是 yield 后面表达式的值

异步的实现

看下面这段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const fs = require('fs');

// 定义一个读取文件的函数,下面所有用到的地方均来自于此
function readFile(filename) {
return new Promise((resolve, reject) => {
fs.readFile(filename, (err, data) => {
if (err) {
reject(err);
} else {
resolve(data);
}
});
});
}

这是node.js中一个简单的读取文件的异步操作,因为用了Promise,所以正常的使用应该是这样的:

1
2
3
4
5
readFile('config.json').then(data => {
console.log(data);
}).catch(err => {
console.error(err);
})

其实这就是上面介绍的Promise对异步的处理。假如我们有这样一个想法,希望代码是这样的:

1
2
3
4
5
6
7
try {
// 同步读取,避免回调
const data = readFile('config.json');
console.log(data);
} catch (err) {
console.error(err);
}

我们知道,正常情况下,这段代码肯定不会如期执行,因为我们的data其实是一个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
// 运行生成器函数的一个容器
// 参数必须是一个生成器
function run(gen) {
// 创建迭代器
const task = gen();
// 开始执行
let result = task.next();

(function step() {
if (!result.done) {
// 用Promise处理
// 解释:无论result.value本身是不是promise对象,都会作为一个promise对象来异步处理
const promise = Promise.resolve(result.value);
promise.then(value => {
// 把本次执行的结果返回
// 也就是语句 const value = yield func(); 的返回值
result = task.next(value);
// 继续
step();
}).catch(err => {
result = task.throw(err);
// 继续
step();
})
}
}());
}

现在,我们有了这样的一个容器run,把读取文件的那段“同步”代码丢进这个容器里:

1
2
3
4
5
6
7
8
9
run(function *() {
try {
// 注意这里多了一个 yield
const data = yield readFile('config.json');
console.log(data);
} catch (err) {
console.error(err);
}
});

现在,我们的代码便能如期的执行了!

简单的解释一下,我们将读取文件的这段“同步”代码包装成了一个生成器函数,然后传给run函数去处理。在run函数内部首先执行这个生成器函数并返回了一个迭代器对象,当第一次执行let result = task.next()的时候,执行的就是readFile('config.json')这句,而这个函数会异步去读取文件并立马返回一个promise对象。所以result的值就是{value: promise, done: false}。由于result.value本身是一个promise对象,所以执行const promise = Promise.resolve(result.value)这句的时候返回的仍然是传入的那个promise对象(也就是result.value)。当读取文件操作完成之后,才会执行thencatch中的代码,在thenresult = task.next(value)这句代码就会让之前卡住的yield readFile('config.json')往后执行,也就是data接收到value的值,然后打印出来。

如果你对迭代器/生成器这块不熟的话,理解起来可能比较痛苦,建议先去补补这方面的知识。

其实,github上已经有人提供了run这样的容器,叫做co。所以,我们只要把注意力放在容器中的生成器里面的代码上面就可以了。

注意点

run容器中yield之后所有的代码都已经是异步执行的了,所以不管yield后面跟的是不是一个promise对象,后面的代码都是异步的。看一个简单的例子:

1
2
3
4
5
6
7
8
9
const add = (a, b) => a + b;

run(function *(){
console.log('run 开始执行');
const sum = yield add(1, 2);
console.log('sum:', sum);
});

console.log('结束了!');

这段代码中yield后面跟的是一个add函数,函数的返回值是一个数值3,并非一个promise对象或其他异步操作。但这段代码执行的结果是:

1
2
3
// run 开始执行
// 结束了!
// sum: 3

哪怕yield后面跟的不是一个函数,直接是一个数值3,执行的结果也是跟上面一样。

为什么?

注意在run中,我们是通过Promise.resolve(result.value)来处理的,result.value就是yield后面跟的东西。对Promise比较熟悉的话应该知道,Promise.resolve()传入的参数如果是一个promise对象,那么直接返回这个对象,如果传入的不是一个promise对象,那么会返回一个新创建的promise对象,并且是完成状态。也就是说Promise.resolve无论如何都会返回一个promise对象,而只有执行了then方法中的result = task.next(value)这句代码之后,yield之后的代码才会继续执行,(sum也才会接收到传过来的值)。因为result = task.next(value)是异步执行的,所以yield之后的代码自然就是异步的了。

async/await

如果你看懂了上面的介绍,那么理解async/await就很轻松了;如果你觉得上面的写法很操蛋,那么下面的写法就是一个字爽。

异步实现

先直接上代码:

1
2
3
4
5
6
7
8
9
10
11
async function run() {
try {
// 这里的 readFile 是上面定义的函数
const data = await readFile('config.json');
console.log(data);
} catch(err) {
console.error(err);
}
}

run();

就是这么简单!一眼看上去,跟上面第3部分的代码有些相像,只是yield变成了await*变成了async,外面多了一个容器run

再对比代码的执行顺序:

1
2
3
4
5
6
7
8
9
10
const add = (a, b) => a + b;

async function run(){
console.log('run 开始执行');
const sum = await add(1, 2);
console.log('sum:', sum);
}

run();
console.log('结束了!');

执行结果:

1
2
3
// run 开始执行
// 结束了!
// sum: 3

有木有很惊讶?就连执行的顺序都跟yield实现的方式一样。而且再也不用管什么容器了,看上去更加直观。这就是所谓的用同步的代码方式去写异步的操作,借用一下老外的说法:让那些烦人的回调见鬼去吧。

虽然这里不用管什么运行容器之类的东西了,但是理解它实现的原理还是很重要的。我不知道async/await是否可以理解成yield实现异步的语法糖,只不过async/await纳入ES7的标准了,而yield的写法是我们自己实现的(比如运行容器run就是我们自己封装的,你也可以根据需求扩展出更强大的功能来)。

最后

感谢阅读和分享!