什么是异步
所谓”异步”,简单说就是一个任务不是连续完成的,可以理解成该任务被人为分成两段,先执行第一段,然后转而执行其他任务,等做好了准备,再回过头执行第二段。
相应地,连续的执行就叫做同步。由于是连续执行,不能插入其他任务。
例如:
setTimeout(function(){
XXX;
},1000)
console.log("complete!");
setTimeout就是一个异步任务,任务分为两个部分,一个部分是延迟1000ms,另一个部分是执行XXX。setTimeout是先执行1000ms的延迟,然后再延迟期间将执行权交给了console.log,输出了“complete!”之后,再去执行setTimeout任务的第二部分,执行XXX。
如果是同步的话,它会等到1000ms过去,执行XXX之后,再输出”complete!”,显然JavaScript不想这样浪费时间。
异步编程方案
异步编程也有多种解决方案,其演变过程是:回调函数 —> Promise —> Generator —> async/await,每个新的演变都解决了之前的一些痛点。
回调函数
所谓回调函数,就是把任务的第二段单独写在一个函数里面,等到重新执行这个任务的时候,就直接调用这个函数。callback的意思就是“重新调用”。
读取文件进行处理,是这样写的:
fs.readFile('/etc/passwd', 'utf-8', function (err, data) {
if (err) throw err;
console.log(data);
});
上面代码中,readFile函数的第三个参数,就是回调函数,也就是任务的第二段。等到操作系统返回了/etc/passwd这个文件以后,回调函数才会执行。
也就是说在执行readFile的时候,执行权交给了主线程执行栈的同步任务,同步任务执行完毕后才会执行这个readFile的回调。
回调函数的缺点
回调函数会导致回调地狱。
比如现在有多个异步任务,且任务有依赖关系(一个任务需要拿到另一个任务成功后的结果才能开始执行)的时候,回调的方式写出来的代码就会像这样:
getData1(data1 => {
getData2(data1, data2 => {
getData3(data2, data3 => {
getData4(data3, data4 => {
getData5(data4, data5 => {
// 终于取到data5了
})
})
})
})
})
这种多层嵌套的结构就是回调地狱,这种情况下代码的可读性很差。
Promise
因此就出现了Promise。Promise的关键点就是将回调函数的嵌套改为了链式调用。
我们使用new Promise去创建一个Promise实例,这个Promise实例会传入一个函数作为参数,函数又有两个函数作为参数,分别是:resolve、reject。
执行resolve函数,Promise实例的状态会变为fulfilled,后续就会去执行.then回调函数
执行reject函数,Promise实例的状态会变为rejected,后续就会去执行.catch回调函数,或者.then的第二个回调函数。
3种状态
Promise实例有三种状态:
pending(进行中)fulfilled(已成功)rejected(已失败)fulfilled和rejected这两种状态又归为已完成状态。
resolve和reject
调用resolve和reject能将分别将promise实例的状态变成fulfilled和rejected,只有状态变成已完成(即fulfilled和rejected之一),才能触发状态的回调。
resolve和reject两种函数只会执行一种,执行了其中一个之后就不会执行另一个了。
Promise至多只能有一个决议值(完成或拒绝)。如果你没有用任何值显式决议,那么这个值就是undefined,这是js常见的处理方式。
如果使用多个参数调用resolve(..)或者reject(..),第一个参数之后的所有参数都会被默默忽略。
因此,如果要传递多个值,你就必须要把它们封装在单个值中传递,比如通过一个数组或对象。
基本结构
let p = new Promise((resolve,reject)=>{
//做一些事情
//然后在某些条件下resolve,或者reject(以下代码)
if(/* 条件随便写 */){
resolve()
} else{
reject()
}
})
p.then(()=>{
//如果p的状态被resolve了,就进入这里
},()=>{
//如果p的状态被reject
})
简单来说,这种.then.then.then的编码方式,就是Promise。
它的异步体现在,new Promise部分是立即执行的,那执行完毕后,就会将执行权交给主线程执行栈的同步任务,同步任务执行完毕后才会去执行.then回调。这里涉及JavaScript事件执行机制Eventloop。
吞掉错误或异常
var p = new Promise(function(resolve, reject) {
foo.bar(); // foo未定义,这里会出错
resolve(42); // 永远不会到达这里
});
p.then(function fulfilled() {
// 永远不会到这里
}, function rejected() {
// err将会是一个来自foo.bar()这一行的TypeError异常对象
})
foo.bar()中发生js错误导致了Promise拒绝,你可以捕捉并对其作出响应。
但如果是在then(..)注册的回调中出现js异常错误呢?以上这种写法是没有办法捕捉到错误的:
var p = new Promise(function(resolve, reject) {
resolve(42);
});
p.then(function fulfilled(msg) {
foo.bar(); // 出错
console.log(msg); // 永远不会到达这里
}, function rejected(err) {
// 永远不会到达这里
});
因为.then的第二个function是侦听new Promise中是否有出错的,而不是侦听.then回调这个Promise是否有出错的。所以如果想要侦听这个回调里是否有出错,需要后面再加一个.then用于去侦听p.then这个Promise是否有出错。
而且根据Promise最基本的特征:Promise一旦决议就不可再变。p已经完成为值42,所以之后查看p的决议时,并不能因为出错就把p变为一个拒绝。
Promise实现多个异步任务顺序执行
如果Promise的.then回调内的函数执行的是同步任务,那么在函数内直接用return将结果返回,下一个.then回调能够正常获取return的值:
var p = Promise.resolve(21);
p.then(function(v) {
console.log(v); // 21
return v*2;
})
// 这里是链接的promise
.then(function(v) {
console.log(v); // 42
});
但如果第一个.then回调的函数内执行的是异步任务,想让第二个.then回调等待第一个.then回调执行完后再执行则不能直接使用return,需要返回一个new Promise,包裹住异步任务来实现等待:
var result=new Promise(function(resolve,reject){
setTimeout(function(){
resolve("one");
},3000)
}).then(function(data){
console.log(data);
return new Promise(function(resolve,reject){
setTimeout(function(){
resolve("two");
},3000)
})
}).then(function(data){
console.log(data);
})
Promise实现多任务并行(即A、B任务都执行完毕了才能执行C任务):
var p1=new Promise((resolve,reject)=>{
resolve('hello');
})
p1.then(result=>result);
var p2=new Promise((resolve,reject)=>{
resolve('hi');
})
p1.then(result=>result);
var p=Promise.all([p1,p2]);
p.then(result=>console.log(result));
这里主要用到Promise.all这种结构。Promise.all([p1,p2])接受一个数组作为参数,数组的元素都是Promise实例,只有当数组中所有Promise实例的状态都变为fulfilled的时候,这个Promise.all的实例才会变为fulfilled,才能执行后续的.then操作。
和Promise.all相关的还有Promise.race,那Promise.race实例的状态是等于其参数中第一个执行完毕的Promise实例的状态,它有可能是rejected,也有可能是fulfilled。
建立可信任的Promise
下面这种情况下,promise p1和promise p2的行为是完全一样的:
var p1 = new Promise(function(resolve, reject) {
resolve(42);
});
var p2 = Promise.resolve(42);
而如果向Promise.resolve(..)传递一个真正的Promise,就只会返回同一个promise:
var p1 = Promise.resolve(42);
var p2 = Promise.resolve(p1);
p1 === p2; // true
Promise的缺点
Promise的最大问题是代码冗余:原来的任务被Promise包装了一下,不管什么操作,一眼看去都是一堆then,原来的语义变得很不清楚。
Generator
Generator函数是ES6提供的一种异步编程解决方案,Generator就能够解决上述Promise代码冗余的问题。它能够以一种类似同步的写法来执行一些异步操作。
- 形式上
Generator函数是一个普通函数,有两个特征:1)function关键字与函数名之间有个星号; 2)函数内部使用yield表达式,定义不同的内部状态 - 执行
Generator函数会返回一个遍历器对象 - 调用
Generator函数后,该函数并不执行,返回的不是函数运行结果,而是一个指向内部状态的指针对象 - 必须调用遍历器对象的
next方法,使得指针移向下一个状态,输出返回的结果
Generator函数的写法
function* helloWorldGenerator() {
yield 'hello';
yield 'world';
return 'ending';
}
var hw = helloWorldGenerator();
hw.next()
// { value: 'hello', done: false }
hw.next()
// { value: 'world', done: false }
hw.next()
// { value: 'ending', done: true }
hw.next()
// { value: undefined, done: true }
输入和输出
生成器函数虽然是特殊的函数,有新的执行模式,但是它仍然有一些基本的特性没有改变。比如,它仍然可以接受参数(即输入),也能够返回值(即输出)。
生成器提供更强大的内建消息输入输出能力,通过yield和next(..)实现:
function *foo(x) {
var y = x * (yield);
return y;
}
var it = foo(6);
// 启动foo(..)
it.next();
var res = it.next(7);
res.value; // 42
第一个it.next()的作用是启动foo生成器,遇到一个yield表达式时暂停,并在本质上要求调用代码为yield表达式提供一个结果值。
接下来调用it.next(7),这一句把值7传回作为被暂停的yield表达式的结果。此时y=6*7=42。所以返回42。
消息是双向传递的——yield..作为一个表达式可以发出消息响应next(..)调用,next(..)也可以向暂停的yield表达式发送值。
function *foo(x) {
var y = x * (yield "Hello"); // yield一个值
return y;
}
var it = foo(6);
var res = it.next();
res.value; // "Hello"
res = it.next(7); // 将7传递给暂停的yield表达式
res.value; // 42
yield..和next(..)这一对组合起来,在生成器执行过程中构成了一个双向消息传递系统。
我们并没有向第一个next()调用发送值,这是有意为之。只有暂停的yield才能接受这样一个通过next(..)传递的值,而在生成器的起始处我们调用第一个next()时,还没有暂停的yield来接受这样一个值。第一个next()一般是用于启动生成器,获得第一个yield出来的值(如果有的话)。
异步应用
因为yield能够中断执行代码的特性,可以帮助我们来控制异步代码的执行顺序。
例如有两个异步的函数 A 和 B, 并且 B 的参数是 A 的返回值,也就是说,如果 A 没有执行结束,我们不能执行 B。
那这时候我们写一段伪代码:
function* effect() {
const { param } = yield A();
const { result } = yield B(param);
console.table(result);
}
const iterator = effect()
iterator.next()
iterator.next()
co库可以用来每次执行A()/b()的请求结束之后,都会自动执行next()方法。
使用Generator去实现Promise的任务顺序执行
我们再回顾一下Promise版本:
var result=new Promise(function(resolve,reject){
setTimeout(function(){
resolve("one");
},3000)
}).then(function(data){
console.log(data);
return new Promise(function(resolve,reject){
setTimeout(function(){
resolve("two");
},3000)
})
}).then(function(data){
console.log(data);
})
Generator版本:
function f1() {
setTimeout(function() {
g.next("one"); //将参数传给data1
}, 3000);
}
function f2(data1) {
console.log("接收到了" + data1);
setTimeout(function(){
g.next("two"); //将参数传给data2
},3000)
}
function* mygenerator() {
var data1 = yield f1();
var data2=yield f2(data1);
console.log(data2);
}
var g = mygenerator();
g.next();
在全局作用域下的g.next()是用于开始迭代器的遍历,执行f1()。当f1()执行到g.next("one")时,第一个yield表达式的值就会被赋值为"one",然后执行f2(data1)。当f2()执行到g.next("two")时,第二个yield表达式的值就会被赋值为"two"。
捕获错误
使用try...catch:
function* gen(x){
try {
var y = yield x + 2;
} catch (e){
console.log(e);
}
return y;
}
var g = gen(1);
g.next();
g.throw('出错了');
// 出错了
try...catch是无法捕获promise抛出的异常的,promise抛出的异常只能由其.catch回调或者.then的第二个回调捕获。
按理来说try...catch是没有办法捕获异步错误的,而yield暂停使得Generator能够捕获错误。
async和await
async函数就是Generator的语法糖。
形式上的不同:
async函数将Generator函数的星号(*)替换成async- 将
yield替换成await
内置执行器
也就是说async函数的执行,和普通函数一样,只需要一行就可以。不用像Generator函数需要调用next方法才能真正执行。
例如对于一个async函数来说:
const asyncReadFile = async function () {
const f1 = await readFile('/etc/fstab');
const f2 = await readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};
调用时只需要:
asyncReadFile();
更好的语义
async和await比起星号和yield,语义更加清楚了。async表示函数里有异步操作,await表示紧跟在后面的表达式需要等待结果。
返回值是Promise
async函数的返回值是Promise对象,这比Generator函数的返回值是Iterator对象方便多了。你可以用then方法指定下一步的操作。
async函数基本用法
async函数返回一个Promise对象,可以使用then方法添加回调函数。当函数执行的时候,一旦遇到await就会先返回,等到异步操作完成,再接着执行函数体内后面的语句。
async function getStockPriceByName(name) {
const symbol = await getStockSymbol(name);
const stockPrice = await getStockPrice(symbol);
return stockPrice;
}
getStockPriceByName('goog').then(function (result) {
console.log(result);
});
先执行了第一个await后的getStockSymbol(name)函数;得到了股票的名称symbol后,将symbol传给第二个await后面的getStockPrice(symbol)作为参数;最后返回股票价格stockPrice。
async/await错误处理
使用try将await语句包含起来,如果await后的语句执行错误,则错误会被catch捕获:
run();
async function run() {
try {
await Promise.reject(new Error("Oops!"));
} catch (error) {
error.message; // "Oops!"
}
}
run();
async function run() {
const v = null;
try {
await Promise.resolve("foo");
v.thisWillThrow;
} catch (error) {
// "TypeError: Cannot read property 'thisWillThrow' of null"
error.message;
}
}
执行顺序问题
async function async1(){
console.log('async1 start');
await async2();
console.log('async1 end')
}
async function async2(){
console.log('async2')
}
console.log('script start');
async1();
console.log('script end')
// 输出顺序:script start->async1 start->async2->script end->async1 end
当async函数执行的时候,一旦遇到await就会先返回,等到触发的异步操作完成后,再执行函数体后面的语句。可以理解为,是让出了线程,跳出了async函数体。
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function async2() {
console.log('async2');
}
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0)
async1();
new Promise(function(resolve) {
console.log('promise1');
resolve();
}).then(function() {
console.log('promise2');
});
console.log('script end');
执行顺序为:
执行async1的时候遇到await先去执行async2,然后跳出了函数体,去执行后续的代码,然后再回到当前async1函数当中执行await后续语句。