一.什么是async函数
1. 概念
async关键字将函数转为异步函数。
function hello() { return 'world'; } hello(); //返回一个字符串 // async关键字将函数转为异步函数;异步函数将普通函数转为promise async function hello() { return 'world'; } hello(); //返回一个promise对象
await关键字只能用于异步函数中。
async+await 等同于 generator+co
await和yield不同在于,yield不返回任何值;await返回后面Promise对象的返回值或者返回后面的原始数据。
var fnName = async function() { let result = await Promise.resolve(1); console.log(result); //1 } fnName(); // 相当于 const co = require('co'); co(function* fnName() { let result = yield Promise.resolve(1); console.log(result); //1 })
async函数相当于执行器+Generator函数,原理如下:
async function fn() { //.... } // 模拟源码实现 function fn() { // run+Generator--等同于co + Generator return run(function* (){ }) } function run(fn) { return new Promise((resolve,reject) => { const gen = fn(); function next(nextFn) { let next; try { next = nextFn(); //相当于所有的gen.next()方法在try代码块中执行 } catch (error) { reject(error); } if (next.done) { resolve(next.value); // async函数生成的Promise对象的resolved状态是所有的await/yield命令完成, done:true时的值 } Promise.resolve(next.value).then(data => { // await后面不是Promise对象,会被当作Promise对象处理 next(function() {return gen.next(data)})// data的值即await返回的值,yield表达式的值 }).catch(error => { next(function() {return gen.throw(error)}) }) } next(function() {return gen.next()}); // 不直接使用gen.next, 可能直接报错 }) }
2. 特征
1. 比Generator函数的语法更语义化
2. async函数执行返回一个Promise对象。co()方法也返回一个Promise对象
返回的Promise对象状态会等所有await表达式运行完或者遇到return/throw后才会发生变化触发then方法。
async函数的return值,作为async函数调用后生成的Promise对象的回调函数的参数
var fnName = async function() { let result = await Promise.resolve(1); return 2; } const p = fnName(); p.then(result => { console.log(result); // 2 等于done=true时的返回值;一般是return的值,否则undefined }).catch(() => {});
应用:
async函数返回Promise对象最直观的应用是作为map的回调函数
const result = [1,2,3].map(async () => { await Promise.resolve('sth'); return 5; }); // map遍历后的值=回调函数的返回值 // async函数的返回值是Promise对象 console.log('result-->',result); Promise.all(result).then(data => console.log('data-->',data)) // 运行结果是 result-->[Promise,Promise,Promise] data-->[5,5,5]
3.await 后面应该是Promise对象;如果是原始类型/普通对象的值,直接返回。
如果对象是thenable对象,直接将其当作Promise对象处理。
4.await 等待后面的Promise对象返回结果。会拦截async函数内部的执行过程;async函数外部按照事件循环机制运行。
5. 如果函数内部没有await命令;可以当作普通函数来确认执行顺序。
3. 使用形式
// 函数声明 async function fn() { //await Promise对象 // ... } // 函数表达式 const fn = async function() { //... } // 对象方法 const obj = { async fn() { } } // 类方法 class A { constructor() { } async fn() { } } // 箭头函数 const fn = async () => {}
// 回调函数
[1,2,3].map(async () => {
// ...
})
4. 保留运行堆栈和上下文环境
function c() { throw new Error('err') } // a,b函数的功能都是Promise改变状态之后运行c() function a() { const p = Promise.resolve(1); console.log('a'); p.then(res => { console.log('a-->',res); c(); //执行到此处时,a()函数运行结束,上下文环境消失;错误堆栈不包含a }) } async function b() { console.log('b'); const res = await Promise.resolve(2); console.log('b-->',res); c(); //await暂停执行,保留上下文环境;错误堆栈会包含a } a(); b();
知识点: then方法可能会丢失上下文环境;await相当于调用then方法;会进入微任务队列
运行结果如下:
二. async函数生成的Promise对象的状态
1. resolved状态
1. 内部不抛出错误;且内部的Promise对象没有rejected状态
根据原理代码可知,只有当内部遍历器的状态{value: returnValue, done: true}时,状态变为resolved状态,
返回此时对应的value值,该值作为成功回调函数的参数。
要想done=true,则所有的await对应的Promise对象都执行完成(源码内对应yield命令执行完成)。
2. 内部抛出异常;但是进行了捕获;状态为resolved状态
async function test() { try { console.log('--1--'); await Promise.reject(1); console.log('--2--'); await Promise.resolve(2); } catch (err) { console.log('err-->',err) } console.log('--3--'); } test().then(result => { console.log('result-->',result); }).catch(err => { console.log('err-->',err) }) // 运行结果如下: // --1-- // err-->1 // --3-- // result-->undefined
2. rejected状态
只要async函数执行过程中出现一个未捕获的错误,整个Promise对象的状态是rejected。
1. 函数内部手动抛出异常
async function test() { throw new Error('err'); // throw自动触发状态为rejected } const p = test(); p.then( data => console.log('data-->',data) //永不执行 ).catch( err => console.log('err-->',err) ) // 运行结果: err-->Error:err
2. 函数内部所有await对应的Promise对象的状态只要出现一次rejected;
就会抛出异常(参考原理)
async function test() { console.log('--1--'); await Promise.resolve('A'); console.log('--2--'); await Promise.reject('B'); //抛出异常,未捕获之后的代码不再执行 console.log('--3--') await 'C'; } test().then(result => { console.log('result-->',result); //不执行 }).catch(err => { console.log('err-->',err); }); // 运行结果如下 --1-- --2-- err-->B
所以为了代码的可执行性,await命令应该全部放在try...catch代码块中(推荐,一个try...catch就可以)。
或者await后的Promise对象使用catch方法(不推荐,需要每个都写)
三. 应用
1. 实现继发(串行)异步操作--上一个结束再开始下一个
async实现继发异步操作(最简单的实现方式)
<!-- * @Author: LyraLee * @Date: 2019-10-30 08:53:28 * @LastEditTime: 2019-11-09 14:34:58 * @Description: 串行异步操作 --> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>串行异步操作</title> <style> #root { 100px; height: 100px; background-color: #eee; } </style> </head> <body> <div id="root"></div> <script type="module"> // 要求前一个动画执行完再执行下一个;有一个动画出错就不再执行; // 成功返回最后一个返回值;动画失败返回上一个执行成功的返回值 function createAnimate(ele, color) {// 实现每一秒改变一次颜色;定时器同时执行,所以每次差1s return new Promise((resolve, reject) => { setTimeout(function () { ele.style.backgroundColor = color; resolve(color); }, 1000) }) } // const animations = ['0f0','f00','00f'].map(i => createAnimate(i)); // 上面的代码会预加载;相当于并发执行 async function chainAnimationsPromise(ele, animations) { let result; for(let color of animations) { // 想实现异步任务继发执行,要在await后面再生成Promise对象 // 因为Promise生成时立即执行;否则会提前执行。 result = await createAnimate(ele, color); } return result; } const animations = ['#0f0','#f00','#00f']; const rootElement = document.querySelector('#root'); chainAnimationsPromise(rootElement, animations).then(finalResult => { console.log('final-->', finalResult) }); </script> </body> </html>
promise实现继发异步操作--代码逻辑比async复杂;代码量比async大
<!-- * @Author: LyraLee * @Date: 2019-10-30 08:53:28 * @LastEditTime: 2019-11-09 14:31:09 * @Description: 串行异步操作 --> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>串行异步操作</title> <style> #root { 100px; height: 100px; background-color: #eee; } </style> </head> <body> <div id="root"></div> <script type="module"> let createAnimate = function (elem, color) { return new Promise((resolve, reject) => { setTimeout(function () { elem.style.backgroundColor = color; resolve(color) }, 1000) }) } function chainAnimationsPromise(elem, animations) { let result = null;// 变量result用来保存上一个动画的返回值 let p = Promise.resolve(); for (let color of animations) { p = p.then(function (val) { result = val; return createAnimate(elem, color); }); } return p.catch(function (e) { /* 忽略错误,继续执行 */ }).then(function (res) { //res是最后的成功值,有错误时是undefined;result是失败后上次的成功返回值 return res || result; }); } const rootElement = document.querySelector('#root'); const animations = ['#f00', '#0f0', '#00f']; chainAnimationsPromise(rootElement, animations).then(finalResult => { console.log(finalResult) }); </script> </body> </html>
2. 实现并发异步操作
异步操作彼此之间独立,没有相互依赖关系,应该使用并发操作;可以大大降低执行时间。
语法:
async function fn() { await Promise.all([promise1, promise2,....]) //并发执行;彼此独立 }
示例(浏览器环境)
function fetch() { return Promise.resolve({ text() { return new Promise((resolve,reject) => { setTimeout(() => resolve('ok'),1000); }); } }) } const url1 = 'https://api.github.com/users/github/orgs'; const url2 = 'https://api.github.com/users/ANTON072/subscriptions'; const urls = [url1, url2]; /*************************************** * 并发执行 * 执行结果之间彼此独立,互不依赖 * 输出结果也没有顺序要求 * ************************************/ async function test() { console.time(2); await Promise.all(urls.map(url => fetch(url).then(res => res.text()))) console.timeEnd(2) } console.time('1'); test(); //主要是为了说明函数的执行时间是同步任务的执行时间 console.timeEnd('1');
执行结果如下:
1: 0.18408203125ms
2: 1000.796875ms
比对继发执行的效果:
/*************************************** * 继发执行 * 当前的执行结果依赖于上一次的执行结果 * *************************************/ async function test1() { console.time(3); for(let url of urls) { // 继发执行 await fetch(url).then(res => res.text()) } console.timeEnd(3) } test1();
运行结果如下:
3: 2003.61083984375ms
由结果可知:并发执行快于继发执行。
3. 实现按照顺序完成异步操作
异步操作彼此之间没有依赖关系;但是要求输出结果按照某种顺序(入参顺序)
/*************************************** * 顺序执行--Promise实现 * 执行结果之间彼此独立, * 但是输出结果要求按照入参顺序 * *************************************/ function test2() { // urls遍历时已经触发并发异步操作 console.time(4); const promises = urls.map(url => fetch(url).then(res => res.text())); promises.reduce((demo, promise) => { console.log('reduce--') return demo.then(() => { console.log('--then promise--') return promise}).then(data => console.log('promise order-->',data)) }, Promise.resolve()).then(() => console.timeEnd(4)) } test2(); /*************************************** * 顺序执行--async函数实现 * 执行结果之间彼此独立, * 但是输出结果要求按照入参顺序 * *************************************/ async function test3() { console.time(5); const promises = urls.map(url => fetch(url).then(res => res.text())); try { let result; for(let promise of promises) { console.log('for--') result = await promise; console.log('async order-->',result) } } catch(error){ console.log('err-->',error) } console.timeEnd(5) } test3(); // 上面的log是为了联系方式异步方法的执行顺序;
运行结果:
reduce-- reduce-- for-- --then promise-- promise order-->ok --then promise-- promise order-->ok 4: 1004.158935546875ms async order-->ok for-- async order-->ok 5:1004.30810546875ms
虽然效果相似,但是async代码更简洁明了。
四. 顶层await
提案 : 目前浏览器支持;
目的: 解决异步模块加载问题
问题场景:
// await.js 被加载模块代码如下 let output; async function main() { const dynamic = await import(url1); const data = await fetch(url2); // output的值依赖上面结果;继发执行 output = process(dynamic.default, data); }
main(); export { output }
其他模块代码引用该文件,因为main是异步方法,所以引用时可能未执行完成;返回undefined
现行解决方法:
// async函数执行返回一个promise export default main(); export {output}
使用时:
import promise, {output} from './await.js'; promise.then(() => { //确保async函数执行完成 // 使用output })
使用顶层await解决方案:
let output; const dynamic = import(url1); const data = fetch(url2); // output的值依赖上面结果;继发执行 output = process(await(dynamic).default, await data); export {output}
应用:
await imports('/.....')