前言
最近博主在看異步編程的實現方法,從 Promise對象 到 Gerenator函數真的是頭大,會想真的要寫這么復雜的代碼嗎?
回答:當然不會。當我學到async和await的時候才知道原來 Promise對象 和 Gerenator函數都是為它做的鋪墊。
博主建議如果你還不了解什么是異步編程可以去看看JavaScript異步編程的四種方法,看完以后可以看Promise對象 和 Generator函數的異步應用
這篇文章會讓你以同步的方式來寫異步代碼,真的很贊。
概述
在弄清楚什么是async和await之前,你需要先知道一個底層技術。我們需要講解Generator的底層的實現機制---協程,帶你一步一步來弄懂async/await的工作方式。
協程
協程是一種比線程更加輕量級的存在。不了解進程與線程的關系的可以先看這篇文章:進程與線程:形象而不抽象
協程與線程的關系就好像是線程與進程的關系,一個線程上可以存在多個協程,但是在線程上同時只能執行一個協程。
- 比如說,當前執行的是A協程,要啟動B協程,那么A協程就需要將主線程的控制權交給B協程,這就體現在A協程暫停執行,B協程恢復執行;
- 同樣從B協程到A協程也是如此。通常,如果從A協程啟動B協程,我們就把A協程稱為B協程的父協程。
協程其實不是被操作系統內核所管理的,而完全是由程序控制的。這樣帶來的好處是性能得到了很大的提升,不會被線程切換那樣消耗資源。
為了讓你更好的理解協程是怎么執行的,我們結合一段代碼來理解協程的規則:
function* genDemo() {
console.log('開始執行第一段')
yield 'generator 2'
console.log('開始執行第二段')
yield 'generator 2'
console.log('開始執行第三段')
yield 'generator 2'
console.log("執行結束")
return 'generator 2'
}
1 console.log('main 0')
2 let gen = genDemo()
3 console.log(gen.next().value)
4 console.log('main 1')
5 console.log(gen.next().value)
6 console.log('main 2')
7 console.log(gen.next().value)
8 console.log('main 3')
9 console.log(gen.next().value)
10 console.log('main')
- 對於上面的程序,只有一個父協程在主線程上執行,所以開始會打印第一行代碼 main 0
- 接着let gen = genDemo() 會創建一個gen協程,創建之后gen協程並沒有立即執行。要讓gen執行,需要調用gen.next。
- 接着第三行代碼中執行gen.next(),這時父協程暫定執行,協程切換到gen協程執行,所以打印“開始執行第一段”
- 接着遇到yield關鍵字,gen協程停止執行,並把yield關鍵字后面的內容返回給父協程,父協程恢復執行... , 如果協程在執行期間,遇到return關鍵字,那么JavaScript引擎會結束當前協程,並把return關鍵字后面的內容返回給父協程。
小結
1.gen協程和父協程是在主線程上交互執行的,並不是並發執行的,它們之間切換是通過yield和gen.next來配合完成的。
2.當在gen協程中調用yield方法時,JavaScript引擎會保存gen協程的當前調用棧信息,並恢復父協程的調用棧信息。同樣,當在父協程中執行gen.next時,JavaScript引擎會保存父協程的調用棧信息,並恢復gen協程的調用棧信息。讀到這里,相信你已經了解生成器(generator)協程是怎么工作的了吧。
async/await
為了使用更加直觀簡潔的代碼來異步編程,就要學習async和await的工作原理。
async
根據MDN定義,async是一個通過異步執行並隱式返回Promise作為結果的函數。
我們先看看它是怎么隱式返回Promise的,異步執行一會在解釋。
async function foo(){
return 2
}
console.log(fool()) // Promise{<resolved>:2}
執行這段代碼,我們可以看到調用async聲明的foo函數返回一個Promise對象,狀態是resolved。
await
我們知道async函數返回的是一個Promise對象,那么下面我們再結合一段代碼來解釋await是什么。
1 async function foo() {
2 console.log(1)
3 let a = await 100
4 console.log(a)
5 console.log(2)
6 }
7 console.log(0)
8 foo()
9 console.log(3)
你能判斷出打印出來的內容是什么嗎?
我們先站在協程的視角來看看這段代碼的整體執行情況:
- 首先執行第7行代碼打印遲來0,
- 緊接着就是執行foo函數,由於foo函數是被saync標記過的,所以當進入該函數的時候,JavaScript引擎會保存當前的調用棧等信息,然后執行foo函數中的console.log(1)語句,並打印出1。
- 緊接着就是執行foo函數中的await 100這個語句了,這里是我們分析的重點,因為在執行await 100這個語句時,JavaScript引擎會在背后為我們默默做了太多的事情,那么下面我們就把這個語句拆開,來看看那JavaScript到底做了什么事情。
當執行到await 100時,會默默創建一個Promise對象,代碼如下:
let promise = new Promise((resolve,reject){
resolve(100)
})
- JavaScript引擎會把resolve(100)這個任務提交給微任務隊列。如果你還不知道什么是微任務,請看宏任務與微任務
- 然后JavaScript引擎會暫停當前協程的執行,將主線程的控制權交給父協程執行,同時會將promise對象返回給父協程。
- 這時候父協程會調用promise.then來監控promise狀態的變化。
- 接下來繼續執行父協程的流程,執行第9行代碼,打印3。
- 隨后,父協程即將執行結束
- 在結束之前,會進入微任務檢查點,然后執行微任務隊列,微任務隊列中有resolve(100)的任務等待執行,執行到這里,會觸發promise.then中的回調函數,回調函數被激活后,將主線程的控制權交給foo協程,並將value值傳給foo函數協程,然后foo函數協程把value的值賦給變量a,然后foo協程繼續執行后面的語句,執行完成以后,會把控制權歸還給父協程。
以上就是await/async的執行流程。正是因為async和await在背后為我們做了大量的工作,所以才可以能用同步的方式寫出異步代碼來。
總結
1.使用async和await可以實現用同步代碼的風格來編寫異步代碼。這是因為async/awiat的基礎技術使用了生成器和Promise。
2.另外v8引擎還為async/await做了大量的語法層面的包裝。
思考題
留個代碼供大家思考,你能分析出這段代碼執行后輸出的內容嗎?
async function foo() {
console.log('fool')
}
async function bar(){
console.log('bar start')
await foo()
console.log('bar end')
}
console.log('script start')
setTimeout(function(){
console.log('setTimeout')
},0)
bar()
new Promise(function(resolve,reject){
console.log('promise executor')
resolve()
}).then(function(){
console.log('promise then')
})
console.log('script end')
歡迎在評論區分享你的答案。