整個文章是我在不斷學習的時候不斷更新的,因此有些知識點可能重復,由于一直在復習不同知識點,因此短期內(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í)行中的順序
- 遇到 await 后,先執(zhí)行 await 后面的表達式,然后將 await 及 async 函數(shù)體剩下的代碼推入微任務隊列
- Promise 本身屬于宏任務
- then(),catch(),finally()等Promise原型鏈上的方法在執(zhí)行時,會判斷是否有合適的Promise狀態(tài),如果能夠執(zhí)行,Promise會調(diào)用該方法并則將其推入微任務隊列。
- 如果多個 await 表達式,第一次 await 執(zhí)行完表達式后推入微任務,當宏任務執(zhí)行完并執(zhí)行微任務時,在碰到第二個 await 時,執(zhí)行完表達式后會再次將 async 函數(shù)體剩下的代碼推入微任務隊列
- 如果 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 end和async1 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ū)別