超詳細的 async / await

整個文章是我在不斷學習的時候不斷更新的,因此有些知識點可能重復,由于一直在復習不同知識點,因此短期內(nèi)沒時間檢查整個文章,如發(fā)現(xiàn)有錯誤,希望能留言提醒我,感激不盡!

async / await 是ES7新增的語法糖,被稱為異步的終極解決方案。
廢話不多說,直接上菜。

目錄:
1.async / await 特點
2.await 和 async 在JS執(zhí)行中的順序
3.async 并發(fā)和繼發(fā)執(zhí)行
4.頁面加載時 defer 屬性和 async 屬性的區(qū)別

async / await 特點

1.await 必須在 async 函數(shù)中(nodejs環(huán)境下)
2.await 后面可以是任意值
3.async 函數(shù)返回的一定是 Promise 對象。如果 async 未返回Promise對象,那么會執(zhí)行立即完成的Promise.resolve(value),無返回值則執(zhí)行Promise.resolve(undefined)
4.await 后的 Promise 對象如果不是fulfilled狀態(tài),則 async 函數(shù)立即結束并返回該 Promise

function a(){
    return new Promise((res, rej) => {console.log(1); [rej(4);]});
    //返回 pending/rejected狀態(tài)的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狀態(tài)的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.內(nèi)置執(zhí)行器

實現(xiàn)方法:

// 函數(shù)聲明
async function foo() {}

// 函數(shù)表達式
const foo = async function () {};
const foo = async () => {};// 箭頭函數(shù)

// 對象的方法
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中的參數(shù)
  }
}
const storage = new Storage();
storage.getAvatar('jake').then(…);

await 和 async 在JS執(zhí)行中的順序

  1. 遇到 await 后,先執(zhí)行 await 后面的表達式,然后將 await 及 async 函數(shù)體剩下的代碼推入微任務隊列
  2. Promise 本身屬于宏任務
  3. then(),catch(),finally()等Promise原型鏈上的方法在執(zhí)行時,會判斷是否有合適的Promise狀態(tài),如果能夠執(zhí)行,Promise會調(diào)用該方法并則將其推入微任務隊列。
  4. 如果多個 await 表達式,第一次 await 執(zhí)行完表達式后推入微任務,當宏任務執(zhí)行完并執(zhí)行微任務時,在碰到第二個 await 時,執(zhí)行完表達式后會再次將 async 函數(shù)體剩下的代碼推入微任務隊列
  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
//控制臺默認會輸出第一次宏任務最后執(zhí)行任務的返回值,如果沒有就是 undefined
//第一次宏任務最后執(zhí)行的任務的是console.log(),無返回值
//Promise等屬于微任務,setTimeout()的回調(diào)函數(shù)是推入下一次宏任務隊列

案例中備注了一個關鍵點,基本所有的博客中都沒寫這個,所以對于原理沒搞透的人來說上面的例子有點難以理解:

  • 為什么async4 end后面不是async3 end而是async2 endasync1 end
    原因:Promise.resolve() 是立即執(zhí)行的,但后面的 then() 在接受到Promise結果后會把自己推入微任務隊列(Event Loop細節(jié)請看我的另外一篇文章)。而函數(shù) async4 在執(zhí)行完 await 后的表達式之后,會類似于 then() 函數(shù)將 async() 函數(shù)剩下的代碼推入微任務隊列并跳出 async() 函數(shù)體,這個時候函數(shù) async3 中的Promise.resolve(async4())沒有返回Promise對象,因此后面的 then() 函數(shù)沒有推入微任務隊列,而是繼續(xù)往下執(zhí)行了console.log('async3 script end'),這是我標記的關鍵點位置,如果沒有這句話,很難理解為何console.log('async3 end');在微任務的末尾。由于執(zhí)行了關鍵點console.log('async3 script end'),因此代表函數(shù) async3() 執(zhí)行完畢,函數(shù) async2() 中的Promise.resolve(async3())執(zhí)行完畢,得到Promise對象(async函數(shù)執(zhí)行完一定會返回一個 fulfilled 狀態(tài)的 Promise 對象),于是 then() 函數(shù)推入微任務隊列, async1() 函數(shù)同理。于是執(zhí)行微任務隊列時,其順序就是'async4 end' 'async2 end' 'async1 end',在async 4 end輸出后,函數(shù) async4() 才徹底結束,返回一個Promise對象,函數(shù) async3() 中的 then() 才有機會推入微任務隊列(then() 函數(shù)是在接收到合適的 Promise 對象時才會推入微任務隊列,否則只是加入緩存,小知識點)。

再看一個案例:

function a() {
    console.log("執(zhí)行函數(shù)a");                   //2
    return Promise.resolve("a函數(shù)return");     //8
}

function b() {
    console.log("執(zhí)行函數(shù)b");                   //6
    return "b函數(shù)return";                      //5
}

async function foo() {
    console.log("函數(shù)foo開始執(zhí)行");             //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

輸出結果:

函數(shù)foo開始執(zhí)行
執(zhí)行函數(shù)a
Promise開始
宏任務隊列結束
a函數(shù)return
執(zhí)行函數(shù)b
Promise的resolve
b函數(shù)return

上面代碼中console.log(v2);會在promise.then((val)=> console.log(val));之后執(zhí)行,因為執(zhí)行const v2 = await b();時,b() 執(zhí)行完畢后會被推入微任務隊列,然后按順序執(zhí)行宏任務和微任務隊列,而此時宏任務隊列為空,微任務隊列為(val)=> console.log(val); console.log(v2);

如果給函數(shù)a加上async:

async function a() {
    console.log("執(zhí)行函數(shù)a");
    return Promise.resolve("a函數(shù)return");
}

輸出結果:

函數(shù)foo開始執(zhí)行
執(zhí)行函數(shù)a
promise開始
宏任務隊列結束
promise的resolve
a函數(shù)return
執(zhí)行函數(shù)b
b函數(shù)return

原因是 await 后的 async 函數(shù)執(zhí)行后還需要 resolve,這需要占用一次微任務流程,因此await async function a(){}會比promise.then((val)=> console.log(val));執(zhí)行的更慢,如果函數(shù)a和函數(shù)b一樣,直接返回的是常數(shù),那么就不存在 resolve 阻塞一次進程了。

總結

1. await 后面如果是 async 函數(shù),那么要小心該函數(shù)本身可能就可能導致異步。(異步時間可能不是 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應該是立即執(zhí)行的)。不過即使多等待一次,也應該是在第二個 then() 執(zhí)行之前運行完成,然而最終卻是在第三次清空微任務隊列時執(zhí)行,異步時間從 1 ticks 變成了 3 ticks
2. async 里的 await 會發(fā)生異步,因此如果該函數(shù)被調(diào)用,在當前宏任務中是無返回值的。

async 并發(fā)和繼發(fā)執(zhí)行

繼發(fā)實現(xiàn):

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

并發(fā)實現(xiàn):

//并發(fā) 1
async function foo1() {
    var res = awaitPromise.all([fetch(url1), fetch(url2), fetch(url3)]);
    return"whew all done";
}
//并發(fā) 2
async function foo2(urls) {
    // 并發(fā)讀取 url
    const textPromises = urls.map(async url => {
        const response = await fetch(url);
        return response.text();
    });
    // 按次序輸出
    for (const textPromise of textPromises) {
        console.log(await textPromise);
    }
}
//并發(fā)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)]    // 并發(fā)執(zhí)行 
  for await (let item of arr) {
    console.log(Date.now(), item)   // 按次序輸出
  }
}
test()
// 1575536194608 2000
// 1575536194608 100
// 1575536195608 3000

頁面加載時 defer 屬性和 async 屬性的區(qū)別

它們是在解析html的時候執(zhí)行,還是js在執(zhí)行代碼時候執(zhí)行?
(1)<script src="example.js"></script>沒有 defer 或 async 屬性,瀏覽器會立即加載并執(zhí)行相應的腳本。也就是說在渲染 script 標簽之后的文檔之前,不等待后續(xù)加載的文檔元素,讀到就開始加載和執(zhí)行,此舉會阻塞后續(xù)文檔的加載;
(2)<script async src="example.js"></script>有了 async 屬性,表示后續(xù)文檔的加載和渲染與js腳本的加載和執(zhí)行是并行進行的,即異步執(zhí)行,但是當js腳本加載完畢之后會立即阻塞進程并先解析 js 腳本;
(3)<script defer src="example.js"></script>有了 defer 屬性,加載后續(xù)文檔的過程和和渲染與 js 腳本的加載和執(zhí)行是并行進行的,即異步執(zhí)行,但是 js 腳本的執(zhí)行需要等到文檔所有元素解析完成之后,DOMContentLoaded 事件觸發(fā)執(zhí)行之前。

總結

(1)defer和async在網(wǎng)絡加載過程是一致的,都是異步執(zhí)行的;
(2)兩者的區(qū)別在于腳本加載完成之后何時執(zhí)行,可以看出defer更符合大多數(shù)場景對應用腳本加載和執(zhí)行的要求;
(3)如果存在多個有defer屬性的腳本,那么它們是按照加載順序執(zhí)行腳本的;而對于async,它的加載和執(zhí)行是緊緊挨著的,無論聲明順序如何,只要加載完成就立刻執(zhí)行,它對于應用腳本用處不大,因為它完全不考慮依賴。

本人才疏學淺,如有錯誤敬請指出,感激不盡!

參考:
[1].Promise-MDN
[2].【ES6基礎知識】promise和await/async
[3]. async / await 執(zhí)行順序詳解
[4].async和await
[5].async-并發(fā)執(zhí)行和繼發(fā)執(zhí)行
[6].Promise與async /await異步微任務隊列差異
[7].promise、async/await在任務隊列中的執(zhí)行順序
[8].script標簽中defer和async屬性的區(qū)別

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內(nèi)容