JS非同期スタック追跡のなぜawaitがPromiseに勝るのか?


概要
async/awaitとPromiseの根本的な違いは、await fn()が現在の関数の実行を一時停止し、promise.then(fn)がfn呼び出しをコールバックチェーンに追加した後、現在の関数を実行し続けることである。

const fn = () => console.log('hello')
const a = async () => {
  await fn() //    fn    
}
//    a  ,    fn    
a() // "hello"

const promise = Promise.resolve()
//   fn        ,     fn
promise.then(fn) // "hello"
この違いはスタック追跡の文脈で非常に顕著である。
プロミセチェーンがいつでも処理されていない異常を投げ出すと、JavaScriptエンジンはエラー情報を表示し、有用なスタック追跡を記録します。
開発者として、普通のPromiseを使ってもasync awaitを使ってもいいです。
Promise
一つのシーンを想像して、非同期関数bの呼び出し解析時に、関数cを呼び出します。

const b = () => Promise.resolve()
const a = () => {
    b().then(() => c())
}
aを呼び出すと、以下のように同期します。
  • bを呼び出してPromiseに戻ります。このPromiseは将来のある時点で解決されます。
  • .thenコールバック(実際には呼び出しc())が、コールバックチェーンに追加される(V 8用語では、[…]が解析処理プログラムに追加される)。
  • その後、関数aの本体でコードの実行を完了しました。aは永遠に掛けられません。bに対する非同期の呼び出しが解析されると、文脈はすでに消えてしまいました。
    もしb(またはc)の異常なステップを投げたら何が起こるかを想像してみてください。理想的には、スタック追跡はaを含むべきです。b(またはc)はそこから呼び出されたのですよね?aを参考しないなら、どうやってできますか?
    それを作動させるためには、JavaScriptエンジンは上記のステップ以外にいくつかのことを行う必要があります。機会があれば、キャプチャしてスタック追跡を保存します。
    V 8では、スタックはbに付加されたPromiseを追跡する。Promiseが実現されると、スタック追跡は転送され、必要に応じてcが使用できるようになる。
    
    b()[a] -> b().then()[a] -> c[a?:a]
    スタック追跡を捕捉するには時間が必要である(すなわち性能を低下させる)。これらのスタックの追跡を記憶するにはメモリが必要です。
    async/await
    以下は同じプログラムです。async/awaitを使ってPromiseではなく作成します。
    
    const b = () => Promise.resolve()
    const a = async () => {
      await b()
      c()
    }
    awaitを使用して、await呼び出しでスタック追跡を収集しなくても、チェーンを呼び出すことができます。
    これは可能です。aが掛けられているので、bの解決を待っています。bが異常を投げた場合、スタック追跡は必要に応じて再構築され得る。
    cが異常を投げたら、スタック追跡は同期関数のように構成されてもいいです。このような状況が発生した時、私たちはまだaの文脈にいます。
    以下の提案に従うことにより、JavaScriptエンジンは、スタック追跡をより効率的に処理することができる。
  • はasync/awaitがPromiseに勝ることを偏愛します。
  • @babel/prest envを使用して、不要なasync/await伝送を避ける。
  • 以上はJS非同期スタック追跡のなぜawaitがPromiseの詳細に勝るのか、Javascriptに関する資料は他の関連記事に注目してください。