Async Function 背後的祕密

因爲能力有限,不免會有疏漏不妥之處,還請不吝賜教!也歡迎你們積極討論

前幾天看到一道題async 輸出順序的一道前端面試題疑問html

async function async1() {
  console.log('async1 start')
  await async2()
  console.log('async1 end')
}

async function async2() {
  console.log('async2 start')
  return new Promise((resolve, reject) => {
    resolve()
    console.log('async2 promise')
  })
}

async1()

new Promise(function (resolve) {
  console.log('promise1')
  resolve()
})
  .then(function () {
    console.log('promise2')
  })
  .then(function () {
    console.log('promise3')
  })

本身算了下,獲得跟題主同樣的疑惑,爲何async1 end會跑到promise3的後面,怎麼算都應該在promise2後面前端

個人理解 實際輸出
async1 start async1 start
async2 start async2 start
async2 promise async2 promise
promise1 promise1
promise2 promise2
async1 end promise3
promise3 async1 end

既然理解跟實際結果的有出入,那確定是哪裏理解不到位,先調試看看究竟是哪一段代碼出了問題git

調試代碼

通過調試,發現問題的關鍵是如下代碼github

async function async2() {
  console.log('async2 start')
  return new Promise((resolve, reject) => {
    resolve()
    console.log('async2 promise')
  })
}

爲了演示方便,作了一些修改:面試

new Promise(function (resolve) {
  console.log('tick: 1')
  resolve()
})
  .then(() => console.log('tick:2'))
  .then(() => console.log('tick:3'))
  .then(() => console.log('tick:4'))
  .then(() => console.log('tick:5'))

async function foo() {
  return Promise.resolve()
}
foo().then(() => {
  console.log('after:foo')
})

輸出順序以下:編程

tick:1
tick:2
tick:3
tick:4
after:foo
tick:5

通過反覆調試發現,若是 foo 不加 async 關鍵字,或者不返回 Promise,結果都符合預期,after:foo出如今tick:2後面.而若是這兩個同時出現的時候,按照個人理解after:foo應該出如今tick:3後面,可是實際結果卻比預期額外多一個tick,出如今tick:4後面.我作了張調試的對比圖,能夠比較直觀的感覺到差異:segmentfault

compare.png

這裏說明個人理解不到位,那就須要去研究清楚這段代碼到底發生了什麼.promise

正好以前看過一些詞法語法以及產生式之類的知識,因而想嘗試從 ES 規範中找找,看能不能找到答案,就看成練習如何閱讀規範了。ecmascript

結果證實我仍是太年輕了,剛開始就看的我頭皮發麻,根本看不懂,原本英語對我來講就已是天書了,加上規範中各類首創的語法,真的是要了親命了,不過好在有各路大神和前輩的文章(後面會列出相關的這些文章),講解怎麼去閱讀規範,經過慢慢學習,總算是把涉及到的相關方面都理清楚了.

從 ECMAScript 規範角度去解釋代碼的運行

接下來,嘗試從語言規範的角度去解釋一下如下代碼,但願能跟你們一塊兒從另一個角度去理解這段代碼在實際運行中到底作了什麼.異步

從新放一下代碼,個人理解 async 關鍵字會產生一個 Promise,加上返回的 Promise 最多兩個微任務,而實際運行中倒是多了個微任務,要搞清楚多出的一個是從哪裏來的.

async function foo() {
  return Promise.resolve()
}

先用一張圖理一下總體的流程

限於我這還沒入門的英語水平,就不一一翻譯了,有須要的朋友能夠點擊連接直接看原文,若是跟我同樣英語比較差的,能夠用百度翻譯谷歌翻譯之類的工具。紅框中是涉及到相關章節,後續只解釋其中的關鍵步驟.

async-function.png

步驟解析

EvaluateAsyncFunctionBody

咱們首先找到15.8.4 Runtime Semantics: EvaluateAsyncFunctionBody,這裏定義了AsyncFunction是如何執行的

Runtime Semantics: EvaluateAsyncFunctionBody

關鍵步驟:

  • 1. 執行抽象操做NewPromiseCapability,該操做會返回一個PromiseCapability Record { [[Promise]]: promise, [[Resolve]]: resolve, [[Reject]]: reject },將其賦值給promiseCapability
  • 2. 抽象操做FunctionDeclarationInstantiation執行函數聲明初始化,像參數變量的聲明,各類狀況的說明,跟本文沒有很大關係
  • 3. 若是實例化沒有錯誤,則執行AsyncFunctionStart(promiseCapability, FunctionBody)
  • ...

AsyncFunctionStart

接下來咱們進到 27.7.5.1 AsyncFunctionStart ( promiseCapability, asyncFunctionBody ) 看看AsyncFunctionStart的定義

AsyncFunctionStart

關鍵步驟:

  • 1. 設置runningContextrunning execution context
  • 2. 設置asyncContextrunningContext的副本
  • 4. 設置asyncContext恢復後須要執行的步驟

    • a. 設置resultasyncFunctionBody的執行結果
    • ...
    • e. 若是result.[[Type]]return,則執行Call(promiseCapability.[[Resolve]], undefined, « result.[[Value]] »)
  • ...

這裏關鍵的是第 4 步中的執行步驟,對於咱們要理解的 foo 函數來講,會先執行Promise.resolve(),獲得結果Promise {<fulfilled>: undefined},而後返回,因此result.[[Type]]return,會執行 4.e 這一步.

最終到 4.e 執行Call(promiseCapability.[[Resolve]], undefined, « result.[[Value]] »), Call是一個抽象操做,這句最後至關於轉換成promiseCapability.[[Resolve]](« result.[[Value]] »).promiseCapability是一個PromiseCapability Record規範類型,在 27.2.1.1 PromiseCapability Records 中能看到PromiseCapability Record的定義

Promise Resolve Functions

順着往下找,能找到27.2.1.3.2 Promise Resolve Functions的定義,接下來看看 resolve 都是怎麼執行的.

Promise Resolve Functions

關鍵步驟,主要針對執行 resolve 時傳入參數的不一樣,而執行不一樣的操做

  • resolve 方法接收參數resolution
  • 7. 使用SameValue(resolution, promise)比較resolutionpromise,若是爲 true,則返回RejectPromise(promise, selfResolutionError),個人理解是爲了不自身循環引用,例:

    let f
    const p = new Promise(resolve => (f = resolve))
    f(p)
  • 8 - 12. 若是resolution不是對象,或者resolution是一個對象但resolution.then不是方法,則返回FulfillPromise(promise, resolution),例:

    // 8, resolution 不是對象
    new Promise(r => r(1))
    // 12, resolution.then 不是方法
    new Promise(r => r({ a: 1 }))
    new Promise(r => r({ then: { a: 1 } }))
  • 13. 設置thenJobCallbackHostMakeJobCallback(resolution.then.[[Value]])執行的結果JobCallback Record { [[Callback]]: callback, [[HostDefined]]: empty }
  • 14. 設置 job 爲NewPromiseResolveThenableJob(promise, resolution, thenJobCallback)執行的結果Record { [[Job]]: job, [[Realm]]: thenRealm }

    • 上面這兩步就是關鍵所在,這裏的 job 會額外建立一個微任務,相似下面的僞代碼:

      function job() {
        const resolution = { then() {} }
        const thenJobCallback = {
          [[Callback]]: resolution.then,
          [[HostDefined]]: empty,
        }
        return new Promise(function (resolve, reject) {
          thenJobCallback[[Callback]](resolve)
        })
      }
  • 15. 執行HostEnqueuePromiseJob(job.[[Job]], job.[[Realm]])

    • 這一步也會建立一個微任務,加上 job,若是傳入的 resolution 仍是一個 Promise 的話,那 resolution.then 還會建立一個微任務,這就解釋了,爲何當在 Async Function 中返回 Promise 以後,after:foo會在tick:4以後出來

結論

至此咱們能夠知道中間的三個微任務都是哪裏來的了:

  • HostEnqueuePromiseJob會建立一個微任務,這個微任務執行時,會去執行 NewPromiseResolveThenableJob返回的 job
  • NewPromiseResolveThenableJob返回的 job 執行時會建立一個微任務,當這個微任務執行時,去執行resolution.then
  • 加上若是resolution是一個 Promise,那執行 then 時,還會建立一個微任務

這其中NewPromiseResolveThenableJob返回的 job 就是以前我不知道的那點.這些都是 js 引擎在後面處理的,咱們日常是沒有感知的.若是不經過閱讀規範,估計很難搞清楚這背後都發生了什麼.

其實還有一種方法能夠更接近實際運行的過程,就是去查看規範實現(既 js 引擎,好比 V8)的源碼,不過相對來講可能閱讀規範會比 C++ 的源碼來的更容易一些.

爲了方便記憶和理解,能夠用 Promise 作以下轉換

暫時執行結果是等價的,不過有可能以後會隨着標準的修改,或者 JS 引擎實現的不一樣而有差別.
async function foo() {
  return Promise.resolve()
}
// =>
function foo() {
  const p = Promise.resolve()
  return new Promise(function (resolve, reject) {
    resolve(p)
  })
}
// =>
function foo() {
  const p = Promise.resolve()
  return new Promise(function (resolve, reject) {
    Promise.resolve().then(() => {
      p.then(resolve)
    })
  })
}

這裏再放一張對比圖,你們能夠找找看跟前面一張有什麼不一樣

compare2.png

關於面試時遇到這道題的"解法"

鑑於我也沒有多少面試的經驗,前不久剛搞砸了一場面試 😭,下面純屬我我的的 yy,沒有多少實踐基礎,你們能夠把這看成一個思路做爲參考,若是有不對的地方歡迎補充和討論

當咱們遇到這種題,若是以前有研究過,那能給出正確的答案當然好.不過有可能會遇到一些題,平常使用中,基本上不會遇到,因此基本對這類邊界狀況可能不會有接觸,好比像這道題,不過不知道也有不知道的解法,面試官出題的目的是爲了考察面試者的知識,以掌握面試者的能力.

像這種狀況能夠直接把本身求解的過程描述給面試官,這樣能經過這個過程把本身掌握的相關知識呈現給面試官,這也是面試官所想要的.還能夠請教面試官正確的解法或者若是找到相關資料,從中展示本身的求知慾.也能夠描述本身日常是如何去編寫異步代碼的,若是是順序相關的異步會明確前後順序的使用 then 串聯,或者使用 await 關鍵詞,保證順序是肯定的,而若是是順序不相關的異步,遇到這種狀況也沒太大關係.這能夠展示本身良好的編程能力.

另一個怪異現象

在調試的過程當中發現另一個使人費解的狀況,若是在Promise.resolve()以前加一個await,竟然能讓after:foo提早,排在tick:3後面,這又是一個使人費解的現象.

其實這是由於規範以前針對await作過一次優化,若是await後面跟着的值是一個 Promise 的話,這個優化會少建立兩次微任務,更多詳情能夠查看下面的文章:

Node.js v10中尚未這個優化,因此咱們能夠實際驗證一下:

Comparison of different Node.js

ES 規範閱讀

  • 基礎(這些基礎屬於非必須條件)

    • 文法,語法,詞法之類的基礎知識
    • BNF 產生式
    • 有必定的 JavaScript 基礎

前兩個基礎,若是有了解的話是最好的,沒有也影響不大,至於第三個基礎,若是沒有的話,難度會有點大 😂

推薦資料

在下面的資源中,把推薦閱讀列表讀完,基本上就能自行去閱讀規範了.不過剛開始可能會有一些難度,好比遇到某個語句不知道什麼意思,或者爲何這裏會出現這種語句之類的疑問,這時候能夠經過搜索引擎去搜索相關關鍵字找到答案.另外這些文章是能夠反覆閱讀,也許每次閱讀都會有不同的收穫.

官方的規範有兩個地方能夠看到,https://tc39.eshttps://www.ecma-internationa... 均可以,不過官方的規範都是放在一個頁面上的,每次打開都須要加載全部內容,速度會很是慢.

這裏推薦一個項目read262.用read262的話,能夠分章節閱讀,要查看某個章節,只須要加載那個章節的內容,當須要打開規範多個部分進行對照時會很方便.不過read262會根據 https://tc39.es/ecma262 的更新自動重鍵,因此只有最新的規範內容,若是須要看其餘版本的規範,仍是須要到ECMA-262去看對應的版本.read262能夠直接使用在線版 https://read262.jedfox.com

JS 引擎

推薦一個庫engine262,以前我說看引擎的源碼會更接近實現,只是礙於閱讀難度來講,閱讀規範會更容易一些.其實有一個庫是用 JavaScript 實現的引擎,這樣源碼的閱讀難度顯然小了不少.不過我推薦仍是先去看規範,而後在實際去engine262源碼中查看對應的實現,最後還能夠將代碼下載到本地運行,實際去調試源碼的運行的過程,以印證對規範的理解.

engine262會根據最新的規範去實現,而咱們看的有時候不必定是最新的規範, engine262也沒有依據規範的版本去作標記.這裏有一個小技巧,能夠先找到實現規範對應的源碼,而後看那個文件的提交記錄,找到跟規範修改的時間相吻合的提交,而後去看那個提交中的實現就跟規範中的描述一致了.

寫在最後

這篇文章經過一個例子,展現如何經過閱讀規範去找到隱藏在 JavaScript 代碼背後的祕密.固然若是僅僅只是得出一個結論,其實並無多大意義,像例子中的狀況,屬於很是邊界的狀況,現實中能遇到的機率應該不大.

我但願能經過這篇文章讓你們更多的瞭解規範,而且經過上面列出的資料去學習和掌握如何閱讀規範的技巧,這樣當咱們遇到某些問題時能夠去找到最權威的資料解答疑惑.不過規範中的大多數知識可能對於咱們平常開發都太大幫助,咱們只須要掌握閱讀的技巧,在有須要的時候去翻翻它便可.

相關文章
相關標籤/搜索