Help us understand the problem. What is going on with this article?

何問分かる?Promise に関するクイズ13問【解説付き】

イントロダクション

JavaScript を使う上で非同期処理、特に promise を理解することは非常に重要である。async/await も promise の上に成り立っているので、promise を理解して始めて正しく使うことができる。この記事では JavaScript における非同期処理をどのくらい理解できているかをチェックするための問題を紹介する。

それぞれのコードについて、実行したときにコンソールに何がどういう順序で出力されるかを考えてみよう。なお、コードはブラウザーで実行するものとし、解答は ECMAScript SpecificationHTML Living Standard に沿ったものとなっている。

🚗 初級

問題1.1

const p1 = Promise.resolve(1)
const p2 = p1.then(x => x * 2)
console.log(p1 === p2)

解答

false

promise の then catch メソッドは呼び出すたびに新しい別の promise が作成され、それが戻り値となる。元の promise に変更を加えているわけではない(内部的には promise のイベントリスナーリスト的なものがありそれに追加しているのだが、少なくとも JavaScript のユーザーからは完全に隠蔽されている)。当たり前といえば当たり前だが、勘違いしてはいけない重要なポイントである。

問題1.2

Promise.resolve()
  .then(() => { throw new Error("thrown") })
  .then(() => console.log(1))
  .then(() => console.log(2))
  .catch(() => console.log("caught"))
  .then(() => console.log(3))

解答

caught
3

promise の then コールバックで例外が発生すると、thenの戻り値の promise は失敗したとみなされる。いったん promise が失敗すると、promise チェーン上をたどって catch コールバック (あるいは then の第2引数)に至るまで、then コールバックは無視される。

catch コールバックが成功すれば、catch の戻り値の promise は成功したとみなされ、最後の then コールバックも呼び出されることになる。

問題1.3

Promise.resolve().then(() => {
  return Promise.resolve(Promise.resolve(Promise.resolve(1)))
}).then(console.log)

解答

1

promise の promise の promise...と何重にラップしても、then コールバックに渡されるのはアンラップされた、非 promise の値である。(この意味で then メソッドは promise の flatMap だと見ることもできる。)

問題1.4

Promise.resolve(1).then(
  () => { throw new Error("oops") },
  () => console.log(2)
).catch(() => console.log(3))

解答

3

p.then(f1, f2)f1 が失敗しても f2 は実行されない。p が失敗したときのみ実行される。反対に、p.then(f1).catch(f2)f1 が失敗すると f2 が実行される。

🚄 中級

問題2.1

Promise.resolve().then(() => console.log(1))
console.log(2)

解答

2
1

promise のコールバックは、必ず現在実行されている同期処理が終了してから(=コールスタックが空になってから)実行される。console.log(2) が実行されてからコールスタックが空になるため、1 が後に出力される。

問題2.2

setTimeout(() => console.log(1), 0)
console.log(2)

解答

2
1

前の問題と同じく、setTimeout のコールバックはコールスタックが空になってから実行される。

問題2.3

const p1 = new Promise((resolve) => {
  console.log(1)
  resolve()
})
console.log(2)

解答

1
2

promise のコンストラクターに渡した関数は、同期的にすぐに実行される。ただし、同期的に resolve してもコールバックは同期的に呼び出されない。

問題2.4

Promise.resolve()
  .then(() => console.log(1))
  .then(() => console.log(2))

Promise.resolve()
  .then(() => console.log(3))
  .then(() => console.log(4))

解答

1
3
2
4

promise が成功するとそのコールバックは microtask queue というキュー(待ち行列)に入れられる。このコードが最後まで実行されると、() => console.log(1)() => console.log(3) の順にキューに入った状態になる。このキューはFIFO(最初に入れられたものから最初に処理される)であるため、() => console.log(1) がまず処理され、() => console.log(2) がキューに入れられ、() => console.log(3) が処理され、() => console.log(4) がキューに入れられ...という具合に進む。

問題2.5

!(async () => {
  console.log(1)
  const x = await 1
  console.log(2)
})()
console.log(3)

解答

1
3
2

async function 内のコードは、await 式が評価されるまでは同期的に実行される。そのため、1 が先に出力される。await する値がたとえ promise でなくとも、それ以降の処理は Promise.resolve().then(() => { ... }) と同様に、コールスタックが空になってから(つまり非同期に)実行される。

問題2.6

!(async () => {
  let x = 0
  const p1 = Promise.resolve().then(() => {
    x = 1
    return 10
  })
  console.log(x + (await p1) * x)
})()

解答

10

これは式 x + (await p1) * x がどのような順序で評価されるかを考慮する必要がある。といっても、この場合は左から計算していくだけなのでシンプルである。

  1. 左の x が評価される。この時点では 0なので 0 が返る。
  2. await p1 が評価される。x1 になり、10 が返る。
  3. 右の x が評価され、1 が返る。
  4. (await p1) * x が評価され、10 * 1 = 10 が返る。
  5. x + (await p1) * x が評価され、0 + 10 = 10 が返る。

🚀 上級

問題3.1

setTimeout(() => console.log(1), 0)
Promise.resolve().then(() => console.log(2))

解答

2
1

実行予定の promise のコールバックと MutationObserver のコールバックは microtask queue というキューに入れられる。一方でそれ以外の非同期なコールバックは task queue と呼ばれるものに入れられる。コールスタックが空になるとまず microtask queue から実行される。それが空になるとようやく task queue から1つだけ task が取り出され、また microtask queue が実行され...と繰り返される。

この問題では () => console.log(2) が microtask、() => console.log(1) が task となる。

問題3.2

setTimeout(() => {  // callback1
  console.log(1)
  Promise.resolve().then(() => console.log(2))
}, 0)

setTimeout(() => {  // callback2
  console.log(3)
  Promise.resolve().then(() => console.log(4))
}, 0)

Promise.resolve()
  .then(() => console.log(5))
  .then(() => console.log(6))

解答

5
6
1
2
3
4

1つ前の問題と同様に考えればよい。ただし、microtask queue は空になるまで実行されるということと、task queue は1つづつしか実行されないということに注意する必要がある。

  1. callback1 と callback2 が task queue に入れられ、() => console.log(5) が microtask queue に入れられる。
  2. () => console.log(5) が実行され、() => console.log(6) が microtask queue に入れられる。
  3. () => console.log(6) が実行され、microtask queue が空になる。
  4. callback1 が実行され、() => console.log(2) が microtask queue に入れられる。
  5. () => console.log(2) が実行され、microtask queue が空になる。
  6. callback2 が実行され、() => console.log(4) が microtask queue に入れられる。
  7. () => console.log(4) が実行される。

問題3.3

<script>  // script1
  console.log(1)
  setTimeout(() => console.log(2), 0)
  Promise.resolve().then(() => console.log(3))
</script>
<script>  // script2
  console.log(4)
  setTimeout(() => console.log(5), 0)
  Promise.resolve().then(() => console.log(6))
</script>

解答

1
3
4
6
2
5

この問題のポイントは、script タグが実行し終わって終了タグまで来るとコールスタックが空になり microtask queue が実行される1ということである。そのため、script タグと script タグの間で microtask queue が実行される。しかし、ページ全体のHTMLをパージングし終わるまでが1つの task なので、それまで次の task は実行されない。この点に注意して前の問題と同様に考えれば良い。

  1. script1 が実行され、() => console.log(2) が task queue に、() => console.log(3) が microtask queue に入れられる。
  2. コールスタックが空になり、microtask () => console.log(3) が実行される。
  3. script2 が実行され、() => console.log(5) が task queue に、() => console.log(6) が microtask queue に入れられる。
  4. コールスタックが空になり、microtask () => console.log(6) が実行される。
  5. HTML全体をパースするタスクが終了し、task () => console.log(2) が実行される。
  6. 5 の task が終了し、microtask queue が空なので task () => console.log(5) が実行される。

最後に

JavaScript を普段使いするためには少なくとも初級の問題は理解しておいたほうがよいだろう。microtask と task、コールスタックについては筆者の別記事でより分かりやすく説明している。

注意すべき点として、Node.js はイベントループの仕組みがブラウザのそれとは異なり、ここであげたコードを実行すると違う結果になることがある。また、v11 で非同期処理の挙動が変わっているようなので、使用するバージョンに気をつけたほうがいいだろう。

仕様の該当箇所


  1. script タグが実行し終わると clean up after running script が実行されてその中で microtask checkpoint というものが実行され、microtask queue 内の microtask が実行される。 

righteous
主にJavaScriptとPythonを使う。 あまり日本語に翻訳されていない情報を中心に紹介する。 分かりやすい説明、正確な用語、曖昧さのない文章を心がける。 オブジェクト指向より関数型に興味がある。 ホビープログラマーなのでプロの方からすると突っ込みどころがあるかもしれないですがご指摘いただけるとありがたいです。 感想をコメントやツイッターなどで書いていただけると喜びます。
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした