非同步出現的前提
首先,因為JavaScript語言是單執行緒的*(目標為瀏覽器端,出生即為單執行緒)*,所以就需要非同步,否則JavaScript指令碼智慧自上而下執行,如果在上部存在一些極其複雜的程式碼需要解析很長的時間的話,下面的程式碼就會遭到阻塞,也就是使用者感受到的卡死.
非同步如何實現
由於是單執行緒語言,所以JavaScript實現非同步的方法是通過**事件迴圈(event loop)**來實現非同步.
console.log('I’m first one code!');
setTimeout(() => {
console.log('I’m setTimeout function code!');
})
console.log('I’m last one code! ');
複製程式碼
這段程式碼的執行結果為
I’m first one code!
I’m last one code!
I’m setTimeout function code!
複製程式碼
所以,程式碼並沒有是自上而下執行,setTimeout函式是延遲了一段時間,等其他語句執行完了採取執行,這種情況就為非同步。
eventloop的機制
根據上一部分我們知道,JavaScript把事件分為兩類:同步與非同步
所以JavaScript的執行機制其實是:
- 判斷一個任務是同步任務還是非同步任務,同步進入主執行緒,非同步進入event table
- 非同步任務在event table中註冊函式,滿足該函式觸發條件後,推入事件佇列
- 同步任務在主執行緒按順序執行,當主執行緒空閒時,再去事件佇列中檢視是否有可執行的非同步任務,如果有就進入主執行緒
這個迴圈即為 event loop
非同步也有小區別
我們先看一段程式碼
setTimeout(() => {
console.log('1');
});
new Promise((resolve) => {
console.log('2');
}).then( () => {
console.log('3');
});
console.log('4');
複製程式碼
如果按照我們剛才的理解,這段程式碼的結果應該為 2,4,1,3
But unfortunately!他的結果為 2,4,3,1
這也引入了另外兩個概念:巨集任務與微任務
所以任務應該分為這兩類:
- 巨集任務(macro-task): 包括整段script程式碼,setTimeout,setInterval
- 微任務(micro-task): Promise,catch, finally,process.nextTick(Node端)
所以js的執行機制其實是
先執行一個巨集任務,過程中如果遇到微任務先把他放到微任務的事件佇列中,當巨集任務執行完畢後,再去檢視微任務的事件佇列,將微任務一次執行完,執行完畢後再去進行下一個佇列的巨集任務,以此迴圈.
借用一張圖演示
介紹主角Async
剛突然發現已經繞遠了:car:,趕快漂移回原來的主題,async,一個在ES2017中提出的非同步方案,有人說他是Generator函式的語法糖,只是把Generator函式的 * 替換為 async,把yield替換為await。我們先不討論這句話說得對不對:speak_no_evil:,但它確實是基於了Generator的一種改進,它讓非同步變得更簡單了。
他做了什麼?
如果要簡述的話,一旦你的函式前帶上了async,你的函式返回值就必定是promise物件.(他就像真香定律一樣是沒有可以逃過的)就算你寫的函式裡返回的不是promise,他也會自動用Promise.resolve()包裝起來讓他成為一個promise物件。
所以,如果我們簡單理解async關鍵字的話,他其實就是給函式加上一個標識,說明這個函式內部有非同步操作。
What is await waiting for?
我們再次簡單的介紹以下 await,await 其實等的是右側表示式的結果.
如果右側是一個函式,則是這個函式的返回值。如果是一個值則就為此值.
我們通過一個例子來"見識見識"它
async function fun1(){
console.log('fun1 is started!');
await fun2();
console.log('fun1 ending!');
}
async function fun2(){
console.log('fun2 is running!');
}
fun1();
console.log('script start');
複製程式碼
我們知道,await是通過執行到此時讓出執行緒,通過阻塞後面的程式碼來執行,但我們執行上面的程式碼發現結果為
fun1 is started!
fun2 is running!
script start
fun1 endding!
複製程式碼
注意,這裡fun2先於"script start" 執行,所以 await 的那個表示式的執行順序其實是從右到左,即為執行了fun2後讀到了await關鍵字,然後阻塞後面的程式碼,這點非常重要,因為之前因為"一旦遇到await就立馬退出執行緒,阻塞後面的程式碼"的觀點,認為 await也會阻塞他後面的那個表示式,但其實不然。
await 與 async 的關係就像魚和水, await必須要有async才可以存在,而async卻不一定需要有await。
await 的下一步操作
一般來說 await等到的右側表示式結果有兩種情況:
Promise or Not Promise。
- 如果不是promise,await會阻塞之後的程式碼,就先去執行async外面的同步程式碼,同步程式碼執行完後再回到async內部,把這個不是promise的結果作為await的結果。
- 如果是promise物件,await也會先阻塞async後面的程式碼,然後執行async外面的同步程式碼,等待這個promise物件到達fulfilled狀態後,把 resolve 的引數作為 await的運算結果。其實就是執行了await Promise.resolve(),這裡不做詳細解釋.
Async 與 Promise 和 Generator的一些比較
- 首先Promise的提出是解決了之前令人頭疼的回撥地獄(callback hell)問題,但直觀的看上去其實就像用了一個類庫,通過Promise的api來完成了非同步操作,操作本身的可調控性不是很高,但已經很實用了。
- Generator函式的語義相較於Promise清晰了許多,但問題就是如果他要自動執行的話必須實用任務執行器來自動執行它。
- Async相較於Generator函式實現起來相對簡潔,更貼切語義。它在語言層面提供了Generator的自動執行器,程式碼量也大大減少。
參考連結
個人Github:Reaper622
歡迎學習交流