ES6异步操作和async函数
一.基本概念
(1)异步
- 异步简单说就是一个任务分成两段,先执行第一段,然后转而执行其他任务,等做好了准备,在回过头执行第二段.这种不连续的执行,就叫做异步.
- 相对的,连续的执行就叫做同步.由于是连续执行,不能插入其他任务,所以操作系统从硬盘读取文件的这段时间,程序只能干等着.
(2)回调函数
-
javaScript语言对异步编程的实现,就是回调函数. 所谓回调函数, 就是把任务的第二段单独写在一个函数里面, 等到重新执行这个任务的时候, 就直接调用这个函数. 它的英文名字callback,直译就是"重新调用".
-
读取文件进行处理
fs.readFile('/etc/passwd', function(err, data){ if (err) throw err; console.log(data); });
- readFile函数的第二个参数,就是回调函数,也就是任务的第二段. 等到操作系统返回了
/etc/passwd
这个文件以后,回调函数才会执行. - node.js约定,回调函数的第一个参数, 必须是错误对象err(如果没有错误, 改参数就是null). 执行分成两段,在这两段之间抛出的错误, 程序无法捕捉, 只能当作参数,传入第二段.
- readFile函数的第二个参数,就是回调函数,也就是任务的第二段. 等到操作系统返回了
(3)Promise
-
回调函数本身没有问题,它的问题出现在多个回调函数嵌套. 如果一次读取多个文件,就会出现多重嵌套.代码不是纵向发展而是横向发展,很快就会乱成一团, 无法管理. 这种情况就被称为"回调函数地狱" (callback hell)
fs.readFile(fileA, function(err, data){ fs.readFile(fileB, function(err,data){ //... }); });
-
Promise有一种新的写法,允许将回调函数的嵌套, 改成链式调用(.then). catch方法捕捉执行过程中抛出的错误.
-
Promise的最大问题是代码冗余, 原来的任务被Promise包装了一下, 不管什么操作, 一眼看上去都是一堆then, 原来的语义变的很不清晰.
(4)Generator函数
-
Generator函数是协程在ES6的实现, 最大特点就是可以交出函数的执行权(即暂停执行).
-
整个Generator函数就是封装的异步任务, 或者说是异步任务的容器. 异步操作需要暂停的地方,都用yield语句注明. Generator函数的执行方法如下:
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}
- 上边的代码中,调用Generator函数, 会返回一个内部指针(即遍历器) g. 这是Generator函数不同于普通函数的另一个地方, 即执行它不会返回结果, 返回的是指针对象. 调用只合作呢g的next方法, 会移动内部指针(即执行异步任务的第一段), 指向第一个遇到的yield语句, 上例是执行到
x + 2
为止. - 换而言之,next方法的作用是分阶段执行Generator函数. 每次调用next方法, 会返回一个对象,表示当前阶段的信息(value属性和done属性). value属性是yield语句后面表达式的值, 表示当前阶段的值; done属性是一个布尔值, 表示Generator函数是否执行完毕,即是否还有下一个阶段.
- 上边的代码中,调用Generator函数, 会返回一个内部指针(即遍历器) g. 这是Generator函数不同于普通函数的另一个地方, 即执行它不会返回结果, 返回的是指针对象. 调用只合作呢g的next方法, 会移动内部指针(即执行异步任务的第一段), 指向第一个遇到的yield语句, 上例是执行到
(5)Generator函数的数据交换和错误处理
-
Generator函数可以暂停执行和恢复执行, 这是它能封装异步任务的根本原因.除此之外,它还有两个特性,使它可以作为异步编程的完整解决方案:函数体内外的数据交换和错误处理机制.
-
next方法返回值的value属性,是Generator函数向外输出数据;next方法还可以接受参数,这是向Generator函数体内输入数据.
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}
上边代码中,第一个next方法的value属性,返回表达式
x+2
的值(3).第二个next方法带有参数2,这个参数可以传入Generator函数,作为上一个阶段异步任务的返回结果,被函数体内的变量y接受.因此,这一步的value属性,返回的就是2(变量y的值). -
Generator函数内部还可以部署错误处理代码, 捕获函数体外抛出的错误.
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函数
(一)含义
- ES7提供了async函数,使得异步操作变得更加方便.async函数就是Generator函数的语法糖
依次读取两个文件操作
var fs = require('fs');
var readFile = function (fileName) {
return new Promise(function (resolve, reject) {
fs.readFile(fileName, function(error, data){
if(error) reject(error);
resolve(data);
});
});
};
var gen = function* (){
var f1 = yield readFile('/etc/fstab');
var f2 = yield readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};
写成async函数
var asyncReadFile = async function(){
var f1 = await readFile('/etc/fstab');
var f2 = await readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};
async函数就是将Generator函数的星号(*)替换成async,将yield替换成await,仅此而已.
async函数对Generator函数的改进:
-
内置选择器. Generator函数的执行必须靠执行器, 所以才有了
co
模块, 而asynk
函数自带执行器. 也就是说,async
函数的执行, 与普通函数一模一样, 只要一行.var result = asyncReadFile();
代码调用了
asyncReadFile
函数,然后它就会自动执行, 输出最后结果. 这完全不想Generator函数, 需要调用next
方法,或者用co
模块,才能得到真正执行, 得到最后结果. -
更好的语义.
async
和await
,比起星号和yield
,语句更清楚.async
表示函数里有异步操作,await
表示紧跟在后边的表达式需要等待结果. -
更广的使用性.
co
模块 约定,yield
命令后面只能是Thunk函数或Promise对象,而async
函数的await
命令后面,可以是Promise对象和原始类型的值(数值、字符串和布尔值,但这时等同于同步操作)。 -
返回值是Promise.
async
函数的返回值是Promise对象, 这比Generator函数的返回值是Iterator(迭代程序)对象方便多了. 可以用then
方法指定下一步的操作.
async
函数完全可以看作多个异步操作, 包装成一个Promise对象, 而await
命令就是内部then
命令的语法糖.
(二)语法
async
函数的语法规则总体上比较简单,难点是错误处理机制
async
函数返回一个Promise对象.
async
函数内部return
语句返回的值, 会成为then
方法回调函数的参数.
async function f() {
return 'hello world';
}
f().then(v => console.log(v));
//"hello world"
函数f
内部return
命令返回的值, 会被then
方法回调函数接收到.
-
async
函数内部抛出错误, 会导致返回的Promise对象变成reject
状态. 抛出的错误对象会被catch
方法回调函数接受到.async function f() { throw new Error('出错了');//抛出一个错误 } f().then( v => console.log(v), e => console.log(e) ) //Error: 出错了
-
async
函数返回的Promise对象, 必须等到nebula所有await
命令的Promise对象执行完,才会发生状态改变. 也就是说, 只有async
函数内部的异步操作执行完,才会执行then
方法指定的回调函数. -
正常情况下,
await
命令后边是一个Pormise
对象. 如果不是, 会被转成一个立即resolve
的Promise对象.async function f(){ return await 123; } f().then(v => console.log(v)); // 123
代码中,
await
命令的参数是数值123
, 它被转成Promise对象, 并立即resolve
. -
await
命令后面的Promise对象如果变为reject
状态,则reject
的参数会被catch
方法的回调函数接收到。async function f() { await Promise.reject('出错了'); } f() .then(v => console.log(v)) .catch(e => console.log(e)) // 出错了
上面代码中,await
语句前面没有return
,但是reject
方法的参数依然传入了catch
方法的回调函数。这里如果在await
前面加上return
,效果是一样的。
-
只要一个
await
语句后边的Promise变成reject
,那么整个async
函数都会中断执行.async function f(){ await Promise.reject('出错了'); await Promise.resolve('hello world');//不会执行 }
为避免这样的问题,可以将第一个
await
放在try...catch
结构里面,这样第二个await
就会执行async function f() { try { await Promise.reject('出错了'); }catch(e){ } return await Promise.resolve('hello world'); } f() .then(v => console.log(v)); //hello world
另一个方法是
await
后边的Promise对象再跟一个catch
方面,处理前边可能出现的错误.async function f(){ await Promise.reject('出错了') .catch(e => console.log(e)); return await Promise.resolve('hello world'); } f() .then(v => console.log(v)); //出错了 //hello world
如果有多个
await
命令,可以统一放在try...catch
结构中.async function main() { try { var val1 = await firstStep(); var val2 = await secondStep(val1); var val3 = await thirdStep(val1, val2); console.log('Final: ', val3); } catch (err) { console.error(err); } }
-
如果
await
后边的异步操作出错, 那么等同于async
函数返回的Promise对象被reject
.
async function f() {
await new Promise(function (resolve, reject) {
throw new Error('出错了');
});
}
f()
.then(v => console.log(v))
.catch(e => console.log(e))
// Error:出错了
代码中,async
函数f
执行后,await
后面的Promise对象会抛出一个错误对象,导致catch
方法的回调函数被调用,它的参数就是抛出的错误对象。
防止出错的方法,也是将其放在try...catch
代码块之中。
async function f() {
try {
await new Promise(function (resolve, reject) {
throw new Error('出错了');
});
} catch(e) {
}
return await('hello world');
}
(三)async函数的用法
async
函数返回一个Promise对象,可以使用then
方法添加回调函数。当函数执行的时候,一旦遇到await
就会先返回,等到触发的异步操作完成,再接着执行函数体内后面的语句。
指定多少毫秒之后输出一个值
function timeout(ms) {
return new Promise((resolve) => {
setTimeout(resolve,ms);
});
}
async function asyncPrint(value, ms) {
await timeout(ms);
console.log(value);
}
asyncPrint('hello world', 50);
代码指定50毫秒之后,输出"hello world";
用两行代码实现,封装一个函数B实现让一个函数中间暂停1s继续执行
function b(timer){
return new Promise((resolve,reject) => {
setTimeout(resolve,timer*1000);
});
}
async function A(){
console.log('程序开始执行');
await b(1);
console.log('程序结束执行');
}
A();
(四)注意点
await
命令后边的Promise
对象,运行结果可能是rejected
,所以最好把await
命令放在try...catch
代码块中.
async function myFunction() {
try {
await somethingThatReturnsAPromise();
} catch (err) {
console.log(err);
}
}
// 另一种写法
async function myFunction() {
await somethingThatReturnsAPromise()
.catch(function (err) {
console.log(err);
};
}
- 多个
await
命令后面的异步操作,如果不存在继发关系,最好让它们同时触发.
let foo = await getFoo();
let bar = await getBar();
代码中, getFoo
和getBar
是两个独立的异步操作(即互不依赖),被写成继发关系。这样比较耗时,因为只有getFoo
完成以后,才会执行getBar
,完全可以让它们同时触发。
//写法一
let [foo, bar] = await Promise.all([getFoo(), getBar()]);
//写法二
let fooPromise = getFoo();
let barPromise = getBar();
let foo = await fooPromise;
let bar = await barPromise;
-
await
命令只能用在async
函数之中,如果用在普通函数,就会报错。 -
如果确实希望多个请求并发执行,可以使用
Promise.all
方法。
async function dbFuc(db) {
let docs = [{}, {}, {}];
let promises = docs.map((doc) => db.post(doc));
let results = await Promise.all(promises);
console.log(results);
}
// 或者使用下面的写法
async function dbFuc(db) {
let docs = [{}, {}, {}];
let promises = docs.map((doc) => db.post(doc));
let results = [];
for (let promise of promises) {
results.push(await promise);
}
console.log(results);
}
(五)async与Promise、Generator的比较
假设某个DOM元素上面, 部署了一系列的动画, 前一个动画结束, 才能开始后一个. 如果当中有一个动画出错,就不再往下执行, 返回上一个成功执行的动画的返回值.
Promise的写法
function a(elem, animationis){
//变量ret用来保存上一个动画的返回值
var ret = null;
//新建一个空的Promise
var p = Promise.resolve();
//使用then方法, 添加所有动画
for(var anim of animations){
p = p.then(function(val){
ret = val;
return anim(elem);
});
}
//返回一个部署了错误捕捉机制的Promise
return p.catch(function(e){
//忽略错误,继续执行
}).then(function(){
return ret;
});
}
接着是Generator函数的写法。
function a(elem, animations){
return spawn(function* (){
var ret = null;
try {
for(var anim of animations){
ret = yield anim(elem);
}
}catch(e){
//忽略错误,继续执行
}
return ret;
});
}
上边代码中,使用Generator函数遍历了每一个动画, 语义比Promise写法更清晰, 用户定义的操作全部都出现在spawn函数的内部.这个写法的文图在于, 必须有一个任务运行器,自动执行Generator函数,上面代码的spawn函数就是自动执行器,它返回一个Promise对象,而且必须保证yield语句后面的表达式,必须返回一个Promise。
Async函数的写法
async function chainAnimationsAsync(elem, animations) {
var ret = null;
try {
for(var anim of animations) {
ret = await anim(elem);
}
} catch(e) {
/* 忽略错误,继续执行 */
}
return ret;
}