ES6+ async/await
前面几节我们已经学习了异步的方案 Promise 和 Generator,上一节我们也通过案例使用 Promise + Generator
实现了比较好的异步案。同时我们实现了简版的 co 库,让我们在使用 Generator 处理异步任务时更加方便,但这不是美的案。
本节我们将学习 ES7 推出的 async/await
其特性是对 JS 的异步编程进行了重要的改进,在不阻塞主线程的情况下,它给我们提供了使用同步的风格来编写异步任务的能力。另外,我们要明确的是 async/await
其实是 Promise + Generator
的语法糖,为了帮助我们像写同步一样书写异步,风格更优雅,捕获也更容易。
本节我们将通过对上一节案例的改造。在不需要 co 库的情况下直接使用 async/await
让我们更加深刻地理解异步方案的演变过程。
我们通过案例来讲解 Promise + Generator
在实际应用中的使用,通过 Generator 和 yield 让异步看起来像同步一样执行。但是这样里面存在的问题就是器直接执行,需要手动处理。为了深层回调的问题我们借助了 co 库来帮助我们去执行器,从而了回调地狱的问题。下面是上一节的。
const ajax = function(api) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (api === 'api_1') {
resolve('api_2');
}
if (api === 'api_2') {
resolve();
}
}, )
})
}
function * getValue() {
const api = yield ajax('api_1');
const value = yield ajax(api);
return value;
}
co(getValue()).then(res => {
console.log(res);
})
上面的中 getValue
是器,不能直接,这里用 co 库来进行执行,然后通过 Promise 的链式执行后的结果。但是这里借助了 co 的库,我们其实最希望的是能像执行普通一样直接 getValue
就能执行并得到结果。 async/await
的出现就是为了抹平在时所做的额外步骤。那让我们看看 async/await
是怎么用的:
async function getValue() {
const api = await ajax('api_1');
const value = await ajax(api);
console.log(value)
return value;
}
getValue() // 控制台打印 value的值是:100
上面的中我们可以看出使用 async/await
定义的 getValue
和器 */yield
定义的基本相同,但是在执行时 async/await
定义的直接即可。从这里我们就能看到 async/await
的优点,无需过多的操作非常优雅和简洁。
上面我们基本了解了 async ,下面我们就来看看它的基本使用和需要注意的地方。
定义异步时需要使用 async
和 function
关键字一起来完成,类似器中的 yield
来暂停异步任务,在 async 中使用 await
关键去等待异步任务返回的结果。
async 其本质是 Promise + Generator
组成的语法糖,它为了减少了 Promise 的链式,解放了 Generator 的单步执行。主要语法如下:
async function name([param[, param[, ... param]]]) {
statements
}
上面中的 statements 是主体的表达式,async 可以通过 return
来返回值,这个返回值会被包装成 Promise 实例,可以被链式。下面我们来看两段等价。
// 下面两段时相同的
async function foo() {
return
}
function foo() {
return Promise.resolve()
}
// 下面两段时相同的
async function foo() {
await ;
}
function foo() {
return Promise.resolve().then(() => undefined)
}
上面的两段时相同的,这里我们就不去探究 async 是怎么实现的,其大概原理类似上节写的 co 库,有兴趣的小伙伴可以去 上去看看 async 编译是什么样子的。
当在 async 中返回的是普通值或 await 后跟普通值时,此时的 async 是同步的。在 Promise 中失败是不能被 try...catch
捕获的,需要通过 catch
的方式来捕获。而使用 async 则是可以通过 try...catch
来捕获。
async function foo() {
return new Error('Throw an error');
}
foo().then(res => {
console.log(res)
}).catch(err => {
console.error(err) // Error: Throw an error
})
async function foo2() {
try{
var v = await foo()
console.log(v)
} catch(e) {
console.log(e); // Error: Throw an error
}
}
foo2()
上面的中在执行 foo()
直接抛出了,而 Promise 和 async/await 对的捕获是不同的,我们知道 Promise 是通过 then
中的失败回调和 catch
来捕获的,而 async 使用的是 try...catch
更像同步的方式。
但是有个问题,当程序需要同时处理多个异步任务时,那我们使用 async/await
怎样捕获那个异步任务出现呢?try 块中的只要程序出现就会抛出,但是不知道是哪个异步任务出错了不利于定位问题。如果使用多个 try...catch
:
const task = function (num) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (num === ) {
reject('throw error')
} else {
resolve('imooc');
}
}, )
})
}
async function foo() {
try {
let res1 = await task();
try {
let res2 = await task();
try {
let res3 = await task();
} catch(e) {
console.log('res3', e)
}
} catch(e) {
console.log('res2', e)
}
} catch(e) {
console.log('res1', e)
}
}
foo() // res3 throw error
看到上面的你是不是觉得很难受啊,又回到了嵌套地狱的原始问题了。async 在异常捕获时,没有非常完美的案,这主要源自依赖 try...catch
对的捕获。但有一些还算比较优雅的案,我们已经知道了 async 返回的是 Promise 那么我们是不是可以使用 Promise 的 catch
来捕获呢?答案是当然的呢。
async function foo() {
let res1 = await task().catch(err => console.log('res1', err));
let res2 = await task().catch(err => console.log('res2', err));
let res3 = await task().catch(err => console.log('res3', err));
}
foo() // res3 throw error
上面的看起来就比嵌套的 try...catch
感觉好很多,这也是比较好的式。在使用 catch
时需要弄清楚 Promise 和 async 之关系,不然就很难理解这种写法。
既然 async/await
这么优雅简洁,那在编程的过程中都使用这个就好啦!其实这里是坑,很多时候 async/await
都会被滥用导致程序卡顿,执行时间过长。
async function foo() {
let res1 = await task();
let res2 = await task();
let res3 = await task();
return { res1, res2, res3 }
}
foo()
在很多时候我们会写成这样的,如果后任务依赖前任务这样写完全没问题,但是如果是三个独立的异步任务,那这样写就会导致程序执行时间加长。这样的过于同步化,我们需要牢记的是 await 看起来是同步的,但它仍然属于异步的,最终还是走的回调,只是语言底层给我们做了很多工作。
针对没有关联的异步任务我们需要把它们解开,
const task = function (num) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('imooc ' + num);
}, )
})
}
async function foo() {
let res1Promes = task();
let res2Promes = task();
let res3Promes = task();
let res1 = await res1Promes;
let res2 = await res2Promes;
let res3 = await res3Promes;
console.log({ res1, res2, res3 })
return { res1, res2, res3 }
}
foo(); // { res1: 'imooc 100', res2: 'imooc 200', res3: 'imooc 300' }
这里需要明白的一点是:为什么要把 task 拿到 await 外面去执行呢?await 的本质就是暂停异步任务,等待返回结果,等到结果返回后就会继续往下执行。还要知道的是每个 task 都是异步任务,像之前的那种写法,await 会等待上异步任务完成才会走下。而我们把 task 拿出来了,也就是每个 task 会按照异步的方式去执行。这个时候三个 task 都已经开始执行了,当遇到 await 就只需要等到任务完成就行。所需要的时间是异步任务中耗时最长的,而不是之前的总和。
本节我们主要通过延续上一节的案例,用 async 给出了最优的案,从而完善了整个异步演变的过程,让我们更加清晰地理解为什么会有 Promise?为什么会有器?为什么会有 async/await?由浅入深层层递进地讲解了 ES6 以后对异步任务处理的演变。然后我们主要学习了 async 的基本使用和处理的捕获。最后,我们讲解了如果不滥用 async 的案例,让我们在以后写程序的过程中更加得心应手。