超详细的 async / await

整个文章是我在不断学习的时候不断更新的,因此有些知识点可能重复,由于一直在复习不同知识点,因此短期内没时间检查整个文章,如发现有错误,希望能留言提醒我,感激不尽!

async / await 是ES7新增的语法糖,被称为异步的终极解决方案。
废话不多说,直接上菜。

目录:
1.async / await 特点
2.await 和 async 在JS执行中的顺序
3.async 并发和继发执行
4.页面加载时 defer 属性和 async 属性的区别

async / await 特点

1.await 必须在 async 函数中(nodejs环境下)
2.await 后面可以是任意值
3.async 函数返回的一定是 Promise 对象。如果 async 未返回Promise对象,那么会执行立即完成的Promise.resolve(value),无返回值则执行Promise.resolve(undefined)
4.await 后的 Promise 对象如果不是fulfilled状态,则 async 函数立即结束并返回该 Promise

function a(){
    return new Promise((res, rej) => {console.log(1); [rej(4);]});
    //返回 pending/rejected状态的Promise
    //没有 rej(1)时是 pending,有 rej(1)时是 rejected
}
async function b(){
    await a();
    console.log(2);
}
b();
console.log(3);
//1
//3
//[error: Uncaught (in promise) 4]

解决办法:把 await 放在 try ... catch 结构中或者在 await 后的 Promise 接一个 catch()

function a(){
    return new Promise((res, rej) => {console.log(1); rej(4);}).catch(_=>_);
    //返回 pending/rejected状态的Promise
    //没有 rej(1)时是 pending,有 rej(1)时是 rejected
}
async function b(){
    await a();
    console.log(2);
}
b();
console.log(3);
//1
//3
//2

5.语义化更好(相对于 * 和 yeild )
6.内置执行器

实现方法:

// 函数声明
async function foo() {}

// 函数表达式
const foo = async function () {};
const foo = async () => {};// 箭头函数

// 对象的方法
let obj = { async foo() {} };
obj.foo().then(...)

// Class 的方法
class Storage {
  constructor() {
    this.cachePromise = caches.open('avatars');
  }

  async getAvatar(name) {
    const cache1 = await this.cachePromise;
    return name; //返回的值作为then中的参数
  }
}
const storage = new Storage();
storage.getAvatar('jake').then(…);

await 和 async 在JS执行中的顺序

  1. 遇到 await 后,先执行 await 后面的表达式,然后将 await 及 async 函数体剩下的代码推入微任务队列
  2. Promise 本身属于宏任务
  3. then(),catch(),finally()等Promise原型链上的方法在执行时,会判断是否有合适的Promise状态,如果能够执行,Promise会调用该方法并则将其推入微任务队列。
  4. 如果多个 await 表达式,第一次 await 执行完表达式后推入微任务,当宏任务执行完并执行微任务时,在碰到第二个 await 时,执行完表达式后会再次将 async 函数体剩下的代码推入微任务队列
  5. 如果 await 后面返回的是 async 的 Promise,那么推入微任务队列后,下次取出队列时,还要等待resolve的结果,因此会将再次推入微任务队列。
    4和5的理解直接看如下代码
    async function async1() {
        console.log('async1 start');
        Promise.resolve(async2()).then(() => {
            console.log('async1 end');
        })
    }
    async function async2() {
        console.log('async2');
        Promise.resolve(async3()).then(() => {
            console.log('async2 end');
        })
    }
    async function async3() {
        console.log('async3');
        Promise.resolve(async4()).then(() => {
            console.log('async3 end');
        })
        console.log('async3 script end')//理解的关键点
    }
    async function async4() {
        await console.log('async4');
        console.log('async4 end')
    }
    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');

懂原理的先自己算一下结果,然后看是否正确。
运算结果:

script start
async1 start
async2
async3
async4
async3 script end
promise1
script end
async4 end
async2 end
async1 end
promise2
async3 end
undefined 
setTimeout
//控制台默认会输出第一次宏任务最后执行任务的返回值,如果没有就是 undefined
//第一次宏任务最后执行的任务的是console.log(),无返回值
//Promise等属于微任务,setTimeout()的回调函数是推入下一次宏任务队列

案例中备注了一个关键点,基本所有的博客中都没写这个,所以对于原理没搞透的人来说上面的例子有点难以理解:

  • 为什么async4 end后面不是async3 end而是async2 endasync1 end
    原因:Promise.resolve() 是立即执行的,但后面的 then() 在接受到Promise结果后会把自己推入微任务队列(Event Loop细节请看我的另外一篇文章)。而函数 async4 在执行完 await 后的表达式之后,会类似于 then() 函数将 async() 函数剩下的代码推入微任务队列并跳出 async() 函数体,这个时候函数 async3 中的Promise.resolve(async4())没有返回Promise对象,因此后面的 then() 函数没有推入微任务队列,而是继续往下执行了console.log('async3 script end'),这是我标记的关键点位置,如果没有这句话,很难理解为何console.log('async3 end');在微任务的末尾。由于执行了关键点console.log('async3 script end'),因此代表函数 async3() 执行完毕,函数 async2() 中的Promise.resolve(async3())执行完毕,得到Promise对象(async函数执行完一定会返回一个 fulfilled 状态的 Promise 对象),于是 then() 函数推入微任务队列, async1() 函数同理。于是执行微任务队列时,其顺序就是'async4 end' 'async2 end' 'async1 end',在async 4 end输出后,函数 async4() 才彻底结束,返回一个Promise对象,函数 async3() 中的 then() 才有机会推入微任务队列(then() 函数是在接收到合适的 Promise 对象时才会推入微任务队列,否则只是加入缓存,小知识点)。

再看一个案例:

function a() {
    console.log("执行函数a");                   //2
    return Promise.resolve("a函数return");     //8
}

function b() {
    console.log("执行函数b");                   //6
    return "b函数return";                      //5
}

async function foo() {
    console.log("函数foo开始执行");             //1
    const v1 = await a();//关键点1              //2
    console.log(v1);                           //5
    const v2 = await b();                      //6
    console.log(v2);                           //8
}

foo();

var promise = new Promise((resolve)=> { 
    console.log("promise开始");                 //3
    resolve("promise的resolve");//关键点2       //7
});
promise.then((val)=> console.log(val));

console.log("宏任务队列结束");                  //4

输出结果:

函数foo开始执行
执行函数a
Promise开始
宏任务队列结束
a函数return
执行函数b
Promise的resolve
b函数return

上面代码中console.log(v2);会在promise.then((val)=> console.log(val));之后执行,因为执行const v2 = await b();时,b() 执行完毕后会被推入微任务队列,然后按顺序执行宏任务和微任务队列,而此时宏任务队列为空,微任务队列为(val)=> console.log(val); console.log(v2);

如果给函数a加上async:

async function a() {
    console.log("执行函数a");
    return Promise.resolve("a函数return");
}

输出结果:

函数foo开始执行
执行函数a
promise开始
宏任务队列结束
promise的resolve
a函数return
执行函数b
b函数return

原因是 await 后的 async 函数执行后还需要 resolve,这需要占用一次微任务流程,因此await async function a(){}会比promise.then((val)=> console.log(val));执行的更慢,如果函数a和函数b一样,直接返回的是常数,那么就不存在 resolve 阻塞一次进程了。

总结

1. await 后面如果是 async 函数,那么要小心该函数本身可能就可能导致异步。(异步时间可能不是 1 ticks,此只是本人也没完全搞透,先挖个坑)

var x;
async function foo1(){
    x = await foo2();
}
async function foo2(){
    console.log('foo2 start');
    return Promise.resolve('foo2 end');
};

foo1();

Promise.resolve(1)
.then(_=>{console.log(x); console.log(_); return 2})
.then(_=>{console.log(x); console.log(_); return 3})
.then(_=>{console.log(x); console.log(_); return 4})
.then(_=>{console.log(x); console.log(_);})

正常情况 await 应该是在第一个 then() 之前运行完成,但是 async 使其需要等待 Promise 的 resolve(都这么说,我也不知道为什么,Promise.resolve应该是立即执行的)。不过即使多等待一次,也应该是在第二个 then() 执行之前运行完成,然而最终却是在第三次清空微任务队列时执行,异步时间从 1 ticks 变成了 3 ticks
2. async 里的 await 会发生异步,因此如果该函数被调用,在当前宏任务中是无返回值的。

async 并发和继发执行

继发实现:

//继发 1
async function foo1() {
    var res1 = await fetch(url1);
    var res2 = await fetch(url2);
    var res3 = await fetch(url3);
    return"whew all done";
}
//继发 2 for...of
async function foo2(urls) {
    for (const url of urls) {
        const response = await fetch(url);
        console.log(await response.text());
    }
}

并发实现:

//并发 1
async function foo1() {
    var res = awaitPromise.all([fetch(url1), fetch(url2), fetch(url3)]);
    return"whew all done";
}
//并发 2
async function foo2(urls) {
    // 并发读取 url
    const textPromises = urls.map(async url => {
        const response = await fetch(url);
        return response.text();
    });
    // 按次序输出
    for (const textPromise of textPromises) {
        console.log(await textPromise);
    }
}
//并发3 for...of
function foo3(time) {
  return new Promise(function (resolve, reject) {
    setTimeout(function () {
      resolve(time)
    }, time)
  })
}
async function test () {
  let arr = [foo3(2000), foo3(100), foo3(3000)]    // 并发执行 
  for await (let item of arr) {
    console.log(Date.now(), item)   // 按次序输出
  }
}
test()
// 1575536194608 2000
// 1575536194608 100
// 1575536195608 3000

页面加载时 defer 属性和 async 属性的区别

它们是在解析html的时候执行,还是js在执行代码时候执行?
(1)没有 defer 或 async 属性,浏览器会立即加载并执行相应的脚本。也就是说在渲染 script 标签之后的文档之前,不等待后续加载的文档元素,读到就开始加载和执行,此举会阻塞后续文档的加载;
(2)有了 async 属性,表示后续文档的加载和渲染与js脚本的加载和执行是并行进行的,即异步执行,但是当js脚本加载完毕之后会立即阻塞进程并先解析 js 脚本;
(3)有了 defer 属性,加载后续文档的过程和和渲染与 js 脚本的加载和执行是并行进行的,即异步执行,但是 js 脚本的执行需要等到文档所有元素解析完成之后,DOMContentLoaded 事件触发执行之前。

总结

(1)defer和async在网络加载过程是一致的,都是异步执行的;
(2)两者的区别在于脚本加载完成之后何时执行,可以看出defer更符合大多数场景对应用脚本加载和执行的要求;
(3)如果存在多个有defer属性的脚本,那么它们是按照加载顺序执行脚本的;而对于async,它的加载和执行是紧紧挨着的,无论声明顺序如何,只要加载完成就立刻执行,它对于应用脚本用处不大,因为它完全不考虑依赖。

本人才疏学浅,如有错误敬请指出,感激不尽!

参考:
[1].Promise-MDN
[2].【ES6基础知识】promise和await/async
[3]. async / await 执行顺序详解
[4].async和await
[5].async-并发执行和继发执行
[6].Promise与async /await异步微任务队列差异
[7].promise、async/await在任务队列中的执行顺序
[8].script标签中defer和async属性的区别

你可能感兴趣的:(超详细的 async / await)