Async/Await
Async/Await
async/await是写异步代码的新方式,以前的方法有回调函数和Promise。
async/await是基于Promise实现的,它不能用于普通的回调函数。
async/await与Promise一样,是非阻塞的。
async/await使得异步代码看起来像同步代码,这正是它的魔力所在。
是es7中提出来的异步解决方法,是目前解决异步编程终极解决方案,以promise为基础,其实也就是generator的高级语法糖,本身自己就相当于一个迭代生成器(状态机),它并不需要手动通过 next()来调用自己,与普通函数一样。async就相当于generator函数中的*,await相当于yield,
async 函数是什么
一句话,async 函数就是 Generator 函数的语法糖。()
async 用于申明一个 function 是异步的,而 await 用于等待一个异步方法执行完成。
async
作为一个关键字放到函数前面,用于表示函数是一个异步函数,因为async就是异步的意思, 异步函数也就意味着该函数的执行不会阻塞后面代码的执行。举个例子:
// async函数返回的是一个Promise对象,async函数(包括函数语句、函数表达式、Lambda表达式)会返回一个Promise对象,如果在函数中return一个直接量,async会把这个直接量通过Promise.resolve() 封装成 Promise 对象
async function timeout( ) {
return setTimeout(() => console.log(666),1000)
}
timeout();
console.log('虽然在后面,但是我先执行');
async 函数返回的是一个 Promise 对象。async 函数(包含函数语句、函数表达式、Lambda表达式)会返回一个 Promise 对象,如果在函数中 return 一个直接量,async 会把这个直接量通过 Promise.resolve() 封装成 Promise 对象。
async 函数返回的是一个 Promise 对象,所以在最外层不能用 await 获取其返回值的情况下,我们当然应该用原来的方式:then() 链来处理这个 Promise 对象,就像这样:
async function testAsync( ) {
return 'hello async';
}
testAsync().then((v) => {
console.log(v); // 输出 "hello async"
});
现在回过头来想下,如果 async 函数没有返回值,又该如何?很容易想到,它会返回 Promise.resolve(undefined)。
await
await 表达式会 暂停
当前 async function 的执行,等待 Promise 处理完成。若 Promise 正常处理(fulfilled),其回调的resolve函数参数作为 await 表达式的值,继续执行
async function。
若 Promise 处理异常(rejected),await 表达式会把 Promise 的异常原因抛出。
另外,如果 await 操作符后的表达式的值不是一个 Promise,则返回该值 本身
。
function getSomething( ) {
return 'something';
}
async function testAsync( ) {
return Promise.resolve('hello async');
}
async function test( ) {
const v1 = await getSomething();
const v2 = await testAsync();
console.log(v1, v2);
}
test();
记住,await 关键字只在异步函数内有效。如果你在异步函数外使用它,会抛出语法错误。 注意,当异步函数暂停时,它调用的函数会继续执行(收到异步函数返回的隐式Promise)。
async/await 帮我们干了啥
上面已经说明了 async 会将其后的函数(函数表达式或 Lambda)的返回值封装成一个 Promise 对象,而 await 会等待这个 Promise 完成,并将其 resolve 的结果返回出来。
现在举例,用 setTimeout 模拟耗时的异步操作,先来看看不用 async/await 会怎么写:
function takeLongTime( ) {
return new Promise(resolve => {
setTimeout(() => resolve('long_time_value'), 1000);
});
}
takeLongTime().then((v) => {
console.log('got', v);
});
如果改用 async/await 呢,会是这样:
function takeLongTime( ) {
return new Promise(resolve => {
setTimeout(() => resolve('long_time_value'), 1000);
});
}
takeLongTime().then((v) => {
const v = await takeLongTime();
console.log(v);
});
优势在于处理 then 链
单一的 Promise 链并不能发现 async/await 的优势,但是,如果需要处理由多个 Promise 组成的 then 链的时候,优势就能体现出来了(很有意思,Promise 通过 then 链来解决多层回调的问题,现在又用 async/await 来进一步优化它)。
假设一个业务,分多个步骤完成,每个步骤都是异步的,而且依赖于上一个步骤的结果。我们仍然用 setTimeout 来模拟异步操作:
/** * 传入参数 n,表示这个函数执行的时间(毫秒) * 执行的结果是 n + 200,这个值将用于下一步骤 */
function takeLongTime(n) {
return new Promise(resolve => {
setTimeout(() => resolve(n + 200), n);
});
}
function step1(n) {
console.log(`step1 with ${n}`);
return takeLongTime(n);
}
function step2(n) {
console.log(`step2 with ${n}`);
return takeLongTime(n);
}
function step3(n) {
console.log(`step3 with ${n}`);
return takeLongTime(n);
}
现在用 Promise 方式来实现这三个步骤的处理
function doIt( ) {
console.time('doIt');
const time1 = 300;
step1(time1)
.then(time2 => step2(time2))
.then(time3 => step3(time3))
.then(result => {
console.log(`result is ${result}`);
console.timeEnd('doIt');
});
}
doIt();
// step1 with 300
// step2 with 500
// step3 with 700
// result is 900
// doIt: 1503.638916015625ms
如果用 async/await 来实现呢,会是这样:
async function doIt( ) {
console.time('doIt');
const time1 = 300;
const time2 = await step1(time1);
const time3 = await step2(time2);
const result = await step3(time3);
console.log(`result is ${result}`);
console.timeEnd('doIt');
}
doIt();
结果和之前的 Promise 实现是一样的,但是这个代码看起来是不是清晰得多,几乎跟同步代码一样。
现在把业务要求改一下,仍然是三个步骤,但每一个步骤都需要之前每个步骤的结果:
function step1(n) {
console.log(`step1 with ${n}`);
return takeLongTime(n);
}
function step2(m, n) {
console.log(`step2 with ${m} and ${n}`);
return takeLongTime(m + n);
}
function step3(k, m, n) {
console.log(`step3 with ${k}, ${m} and ${n}`);
return takeLongTime(k + m + n);
}
这回先用 async/await 来写:
async function doIt( ) {
console.time('doIt');
const time1 = 300;
const time2 = await step1(time1);
const time3 = await step2(time1, time2);
const result = await step3(time1, time2, time3);
console.log(`result is ${result}`);
console.timeEnd('doIt');
}
doIt();
// step1 with 300
// step2 with 800 = 300 + 500
// step3 with 1800 = 300 + 500 + 1000
// result is 2000
// doIt: 2903.52001953125ms
除了觉得执行时间变长了之外,似乎和之前的示例没啥区别啊!别急,认真想想如果把它写成 Promise 方式实现会是什么样子?
function doIt( ) {
console.time('doIt');
const time1 = 300;
step1(time1)
.then(time2 => {
return step2(time1, time2)
.then(time3 => [time1, time2, time3]);
})
.then(times => {
const [time1, time2, time3] = times;
return step3(time1, time2, time3);
})
.then(result => {
console.log(`result is ${result}`);
console.timeEnd('doIt');
});
}
doIt();
有没有感觉有点复杂的样子?那一堆参数处理,就是 Promise 方案的死穴—— 参数传递太麻烦了,看着就晕!
注意点
大多数人的误区
async function async1( ){
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function async2( ){
console.log('async2');
}
async1();
console.log('i am koala');
我想会有一些开发者认为await是把同步变为异步,执行顺序是这样
"async1 start"
"async2"
"async1 end"
"i am koala"
然而并不是,正确的执行顺序是
"async1 start"
"async2"
"i am koala"
"async1 end"
解释一下原因:
“ async 函数返回一个 Promise 对象,当函数执行的时候,一旦遇到 await 就会先返回,等到触发的异步操作完成,再接着执行函数体内后面的语句。” ——阮一峰ES6
看不懂打印顺序的话,可以看 这个博客
运行结果是 rejected
await 命令后面的 Promise 对象,运行结果可能是 rejected,所以最好把 await 命令放在 try...catch 代码块中。
/** * await处理catch异常情况 * @param {Promise} promise promise对象 * @returns {Array} */
function awaitWrap(promise) {
if (!promise || !Promise.prototype.isPrototypeOf(promise)) {
return new Promise((resolve, reject) => {
reject(new Error('requires promises as the param'));
}).catch((err) => {
return [null, err];
});
}
return promise
.then(function( ) {
return [...arguments, null];
}).catch(err => {
return [null, err];
});
}
async function testAsync( ) {
return Promise.resolve('hello async');
}
async function test( ) {
const [res, err] = await awaitWrap(testAsync());
console.log(res, err);
}
test();
async 函数是非常新的语法功能,新到都不属于 ES6,而是属于 ES7。目前,它仍处于提案阶段,但是转码器 Babel 和 regenerator 都已经支持,转码后就能使用。
协程
协程,又称微线程,纤程。英文名Coroutine。 协程的概念很早就提出来了,但直到最近几年才在某些语言(如Lua)中得到广泛应用。
子程序,或者称为函数,在所有语言中都是层级调用,比如A调用B,B在执行过程中又调用了C,C执行完毕返回,B执行完毕返回,最后是A执行完毕。
所以子程序调用是通过栈实现的,一个线程就是执行一个子程序。
子程序调用总是一个入口,一次返回,调用顺序是明确的。而协程的调用和子程序不同。
协程看上去也是子程序,但执行过程中,在子程序内部可中断,然后转而执行别的子程序,在适当的时候再返回来接着执行。
注意,在一个子程序中中断,去执行其他子程序,不是函数调用,有点类似CPU的中断。
- 进程>线程>协程
- 协程的第一大优势是具有极高的执行效率,因为子程序切换不是线程切换,而是由程序自身控制,因此没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显;
- 协程的第二大优势是不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多;
- 协程看上去也是子程序,但执行过程中,在子程序内部可中断,然后转而执行别的子程序,在适当的时候再返回来接着执行,需要注意的是:在一个子程序中中断,去执行其他子程序,这并不是函数调用,有点类似于CPU的中断;
- 用汽车和公路举个例子:js公路只是单行道(主线程),但是有很多车道(辅助线程)都可以汇入车流(异步任务完成后回调进入主线程的任务队列);generator把js公路变成了多车道(协程实现),但是同一时间只有一个车道上的车能开(依然单线程),不过可以自由变道(移交控制权);
- 协程意思是多个线程互相协作,完成异步任务,运行流程大致如下:
- 1)协程A开始执行;
- 2)协程A执行到一半,进入暂停,执行权转移到协程B;
- 3)一段时间后,协程B交还执行权;
- 4)协程A恢复执行;
- 协程是一个无优先级的子程序调度组件,允许子程序在特定的地点挂起恢复;
- 线程包含于进程,协程包含于线程,只要内存足够,一个线程中可以有任意多个协程,但某一个时刻只能有一个协程在运行,多个协程分享该线程分配到的计算机资源;
- 就实际使用理解来说,协程允许我们写同步代码的逻辑,却做着异步的事,避免了回调嵌套,使得代码逻辑清晰;
- 何时挂起,唤醒协程:协程是为了使用异步的优势,异步操作是为了避免IO操作阻塞线程,那么协程挂起的时刻应该是当前协程发起异步操作的时候,而唤醒应该在其他协程退出,并且他的异步操作完成时;
- 单线程内开启协程,一旦遇到io,从应用程序级别(而非操作系统)控制切换对比操作系统控制线程的切换,用户在单线程内控制协程的切换,优点如下:
- 1)协程的切换开销更小,属于程序级别的切换,操作系统完全感知不到,因而更加轻量级;
- 2)单线程内就可以实现并发的效果,最大限度地利用cpu;