异步调用的历史
注:本文主要参考阮一峰老师的[es6教程](http://es6.ruanyifeng.com/#docs/async)我们平常希望前一次异步调用的结果发生以后,再产生调用某些函数。比如我们通过node的fs来读取文件,我们需要读取文件A后读取文件B,再读取文件C等。
有什么方式可以实现呢?通过探索,我们发现了以下各种方式的实现。
回调函数
最开始我们用的是回调函数的方法, 但是多次回调会让函数的阅读性大大下降,也不利于我们的调试。 比如像下面这样:
fs.readFile(fileA, 'utf-8', function (err, data) {
fs.readFile(fileB, 'utf-8', function (err, data) {
// ...
});
});
阮一峰老师的书上也是这样表示的:
不难想象,如果依次读取两个以上的文件,就会出现多重嵌套。代码不是纵向发展,而是横向发展,很快就会乱成一团,无法管理。因为多个异步操作形成了强耦合,只要有一个操作需要修改,它的上层回调函数和下层回调函数,可能都要跟着修改。这种情况就称为”回调函数地狱”(callback hell)。
Promise
后来某些大牛发明了Promise,其最大的优势就是大大提高了阅读性。 将嵌套调用变成了链式调用。
像下面这样
var readFile = require('fs-readfile-promise');
readFile(fileA)
.then(function (data) {
console.log(data.toString());
})
.then(function () {
return readFile(fileB);
})
.then(function (data) {
console.log(data.toString());
})
.catch(function (err) {
console.log(err);
});
注意resolve只能传一个参数,需要多个参数时,可以通过对象或者数组的形式传入。
Promise 的最大问题是代码冗余,原来的任务被 Promise 包装了一下,不管什么操作,一眼看去都是一堆then,原来的语义变得很不清楚。
那么,有没有更好的写法呢?
Generator函数
设想一下,我们需要的功能是等待异步调用的结果,再执行某些函数,如果有一个功能,可以在异步调用这里暂停,待结果出来后再执行下面的内容,是不是就达到我们的要求了?
这就是Generator函数
其语法像下面这样
function* asyncJob() {
// ...其他代码
var f = yield readFile(fileA);
// ...其他代码
}
function后的*
表示这是一个Generator函数,里面只要遇到yield,就会等待调用结果。后面的代码不执行。
它的最大优点,就是代码的写法非常像同步操作,如果去除yield命令,简直一模一样
它返回的是一个指针,需要用next调用,每次都返回一个对象,value指当前yield的结果,done指是否结束了。
function* gen(x) {
var y = yield x + 2;
return y;
}
var g = gen(1);
g.next() // { value: 3, done: false }
g.next() // { value: undefined, done: true }
注意这第一个g.next()的value是3,并不是指y是3,而是yield后面的计算x+2
为3
不信我们可以试试打印这个y
function* gen(x) {
var y = yield x + 2;
console.log('y=',y)
return y;
}
var g = gen(1);
第一次执行next,计算yield后面的值,然后暂停,并不会继续计算下去,直到我们再调用next(),此时可以看到返回的是undefined
所以如果我们需要在每次next后提供当前yield的值,需要手动在next里添加。
function* gen(x){
var y = yield x + 2;
return y;
}
var g = gen(1);
g.next() // { value: 3, done: false }
g.next(2) // { value: 2, done: true }
这里第二步的g.next(2)
表示我们从第一个yield获得的结果y为2,所以return的y才为2
错误捕捉
function* gen(x){
try {
var y = yield x + 2;
} catch (e){
console.log(e);
}
return y;
}
var g = gen(1);
g.next();
g.throw('出错了');
// 出错了
上面代码的最后一行,Generator 函数体外,使用指针对象的throw方法抛出的错误,可以被函数体内的try…catch代码块捕获。这意味着,出错的代码与处理错误的代码,实现了时间和空间上的分离,这对于异步编程无疑是很重要的。
async函数
async函数是基于Generator函数的语法糖
其基本语法如下:
const asyncReadFile = async function () {
const f1 = await readFile('/etc/fstab');
const f2 = await readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};
async函数对 Generator 函数的改进,体现在以下四点。
(1)内置执行器。
Generator 函数的执行必须靠执行器,所以才有了co模块,而async函数自带执行器。也就是说,async函数的执行,与普通函数一模一样,只要一行。
asyncReadFile(); 上面的代码调用了asyncReadFile函数,然后它就会自动执行,输出最后结果。这完全不像 Generator 函数,需要调用next方法,或者用co模块,才能真正执行,得到最后结果。
(2)更好的语义。
async和await,比起星号和yield,语义更清楚了。async表示函数里有异步操作,await表示紧跟在后面的表达式需要等待结果。
(3)更广的适用性。
co模块约定,yield命令后面只能是 Thunk 函数或 Promise 对象,而async函数的await命令后面,可以是 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时等同于同步操作)。
(4)返回值是 Promise。
async函数的返回值是 Promise 对象,这比 Generator 函数的返回值是 Iterator 对象方便多了。你可以用then方法指定下一步的操作。
进一步说,async函数完全可以看作多个异步操作,包装成的一个 Promise 对象,而await命令就是内部then命令的语法糖。
其更多的语法细节,还是需要看阮一峰老师完整的教程,在这里就不借花献佛了。
三种形式的比较
下面我们通过一个例子来看看三种形式的比较,我们用setTimout来模拟异步请求,时间到了之后会打出promise和时间
var promise = function(t){
return new Promise((resolve)=>{
setTimeout(resolve(t),t)
})
.then((t)=> {console.log('promise',t)})
}
promise
//promise链式调用
function demoPromise(){
promise(100)
.then(promise(200))
.then(promise(300))
.then(promise(400))
}
demoPromise()
generator
//generator调用
function* demoGenerator(){
try{
var t1 = yield promise(500)
var t2 = yield promise(600)
}catch(err){
console.log(err)
}
}
var g = demoGenerator()
g.next()
g.next()
async
//async调用
async function demoAsync(){
try {
var t1 = await promise(700)
var t2 = await promise(800)
}catch(err){
console.log(err)
}
}
demoAsync()
以上,谢谢.