假如你已经知道了什么是异步,并且已经写过很多的异步代码。这篇文章主要介绍几种对异步代码的处理,即异步编码姿势:
- 回调函数;
- Promise;
- 迭代器、生成器;
- async/await。
重点在第3、4部分。
回调函数
这个没什么好说的,直接看一段代码:
const fs = require('fs');
fs.readFile('config.json', (err, data) => {
if (err) {
console.error(err);
} else {
console.log(data);
}
});
后面部分都以该读取文件操作为例来讲解。
Promise
Promise
就是为异步而生的,主要是为了解决所谓的回调地狱问题。Promise
的三个状态:pending
,fulfilled
和rejected
。
通常的写法:
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()
传入的函数会立即执行,then
跟catch
中传入的函数才是异步执行的。
then
方法何时执行?取决于两点:
-
promise
何时变成完成状态(fulfilled
); - 在异步队列中的位置。
迭代器、生成器
概念的理解
先理解两个概念:生成器是一个返回迭代器的函数;那么迭代器就是生成器执行后返回的结果(对象)。所以,生成器是函数,迭代器是对象(很容易弄混的两个概念)。
首先,生成器是一个函数,这是一个特殊的函数,函数定义如下:
// 这就是一个生成器(函数)
function *createIterator() {
const a = yield 1;
const b = yield a + 2;
yield b + 3;
}
// 这就是一个迭代器(对象)
const iterator = createIterator();
// 注释部分是next方法执行的返回值
iterator.next(); // {value: 1, done: false} 执行完这句并没有给a赋值
iterator.next(); // {value: 3, done: false} 执行这句的时候才会给a赋值1
iterator.next(5); // {value: 8, done: false} 执行这句的时候才会给b赋值5
iterator.next(); // {value: undefined, done: true}
异步的实现
看下面这段代码:
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
,所以正常的使用应该是这样的:
readFile('config.json').then(data => {
console.log(data);
}).catch(err => {
console.error(err);
})
其实这就是上面介绍的Promise
对异步的处理。假如我们有这样一个想法,希望代码是这样的:
try {
// 同步读取,避免回调
const data = readFile('config.json');
console.log(data);
} catch (err) {
console.error(err);
}
我们知道,正常情况下,这段代码肯定不会如期执行,因为我们的data
其实是一个promise
对象。但是假如有这样一个容器,它能如期的执行我们上面的这段代码,我们只需要把代码丢进这个特殊的容器里。注意到没有,上面这段代码其实是一段同步的代码,通过同步的代码实现异步的操作,这似乎是一个很完美的想法,只是首先我们需要有这样的一个容器。
运行容器
运行异步代码的容器:
// 运行生成器函数的一个容器
// 参数必须是一个生成器
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
,把读取文件的那段“同步”代码丢进这个容器里:
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
)。当读取文件操作完成之后,才会执行then
或catch
中的代码,在then
中result = task.next(value)
这句代码就会让之前卡住的yield readFile('config.json')
往后执行,也就是data
接收到value
的值,然后打印出来。
如果你对迭代器/生成器这块不熟的话,理解起来可能比较痛苦,建议先去补补这方面的知识。
其实,github
上已经有人提供了run
这样的容器,叫做co。所以,我们只要把注意力放在容器中的生成器里面的代码上面就可以了。
注意点
在run
容器中yield
之后所有的代码都已经是异步执行的了,所以不管yield
后面跟的是不是一个promise
对象,后面的代码都是异步的。看一个简单的例子:
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
对象或其他异步操作。但这段代码执行的结果是:
// 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
就很轻松了;如果你觉得上面的写法很操蛋,那么下面的写法就是一个字爽。
异步实现
先直接上代码:
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
。
再对比代码的执行顺序:
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('结束了!');
执行结果:
// run 开始执行
// 结束了!
// sum: 3
有木有很惊讶?就连执行的顺序都跟yield实现的方式一样。而且再也不用管什么容器了,看上去更加直观。这就是所谓的用同步的代码方式去写异步的操作,借用一下老外的说法:让那些烦人的回调见鬼去吧。
虽然这里不用管什么运行容器之类的东西了,但是理解它实现的原理还是很重要的。我不知道async/await
是否可以理解成yield
实现异步的语法糖,只不过async/await
纳入ES7的标准了,而yield
的写法是我们自己实现的(比如运行容器run
就是我们自己封装的,你也可以根据需求扩展出更强大的功能来)。
最后
感谢阅读和分享!