Promise, generator, async/await はどのように実行されるかについて

Published: 2022/3/12


microTask と (macro)Task

javascript の実行環境は、外部からイベントがあれば、それに対して callback するイベントループを基本として動作する。 この、一番外側のイベントによって発生する処理を Task, もしくは macroTask と呼ぶ。 macroTask は、おおむね queue みたいなものだと理解すれば良い。 (ただし、実行順序などは多少前後したりするかもしれないので、厳密には queue ではない)

また、特に Promise のために、 microTask というものも導入される。 これは、ある macroTask が完了して次の macroTask の処理にイベントループが移る前に、すべて解消するべき小さなタスクの queue として実装される。

なので、 javascript のランタイムは、大体以下のような挙動をする。

  • while macroTask from macroTasks
    • do macroTask
    • while microTask in microTaskQueue
      • do microTask
  • repeat from beginning

ここで、 macroTask も microTask も実態は callback の関数なので、 タスクの実行とはそれを関数として呼び出すこと、と考えれば良い。

Promise とは: pending -> fulfill/rejected を伝播する機構

Promise 機能は、上記の microTask と macroTask の実行機構があったとすれば、 queueMicrotask 関数があればすべて実装できる。

Promise のデータ構造はおおまかに以下の通りであって、

interface Promise {
  state: 'pending' | 'fulfulled' | 'rejected'
  value?: Data
  reason?: Error
  onResolveCallbacks: ((Data) => void)[]
  onErrorCallbacks: ((Error) => void)[]
}

resolve された瞬間に、それまで自身に対して .then して生成された Promise たちを、その .then の引数関数を実行した結果でもって resolve していくような処理を queueMicrotask する、というのが処理の中心部分。

queueMicrotask があることによって、例えば以下のコードを実行しても Promise の中の式は後から実行されるので、 promise 中のコードによる影響は、後から実行されるものとしてコードを整理すれば良くなる。

const foo () = {
  let a = 1
  Promise.resolve({}).then(() => {
    a = 2
  })
  console.log(a) // ==> 1 が表示
}

generator の実装

例えば、 generator は以下のようなコードであるが、

function* myGen() {
  yield 1
  yield 2
  return 3
}

これは、 yield のタイミングで return と同じように .next() の呼び出し元に戻り、もう一度 next() を呼ばれた際には、 その関数の前回の yield の直後から実行を再開するような機構があれば、これは素直に stack 上に再現できる。

というのも、このように途中から関数の実行を再開するような呼び出しを行うために必要なのは、すべてのローカル変数をクロージャ的にヒープ上に確保しておけば、 generator を実行して得られる iterator オブジェクト自身に前回の yield 場所を保持し、また runtime として(generator)関数の途中からの再開処理を実装すれば、普通に実装が可能。

async/await の実装

generator 処理が実装できている javascript のランタイムがあれば、それは、関数の途中から実行を再開することが可能になっていることに他ならない。

async/await もなので、似たような機構で実装されていて、ただ違いとしては以下の通り。

  1. await のタイミングで、 await した対象に対して .then して、自身の残りを再開するような処理を行う。
  2. await の戻り値は、 1 の結果得られる自身の再開処理の Promise オブジェクト。

補足: それぞれの transpile の方法

機能説明
PromisequeueMacrotask を setImmediate ないし setTimeout で代用
Generatorgenerator の中身を構文解析して、 switch 文で等価な表現を行い、かつそれを generator としてふるまわせる runtime を用意
async / awaitpromise と generator で等価に変換可能

Tags: javascript

関連記事