ES6:async / await ---使用同步方式寫異步代碼


前言

最近博主在看異步編程的實現方法,從 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')

歡迎在評論區分享你的答案。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM