初めに
プロミスでは約束した書き方など、統一されたやり方で従う点がたくさんあるので、構造上の話ではなく、使い方や動きから思ったことをまとめていきたいと思います。
今回主な参考文章はこちらです。
Promises chaining - javascript.info
Error handling with promises - javascript.info
Promises, async/awaitでは各章の基礎概念と使い方にはすごく分かりやすい例を出しているので、ぜひご参考ください。(日本語訳もあります。)
もっと詳しい理論や説明は、こちらをおすすめします。
JavaScript Promiseの本
about then
Thenable object
普通のプロミスでエラーが出ない場合は、プロミスオブジェクトは非同期処理を行い結果をreturn
して、then
でその結果を受け取って、また結果をreturn
したら次のthen
の引数として受け入れます。そうやってプロミスオブジェクトは結果を受け取るたびにプロミスオブジェクトは更新される。(promise
チェーンで結果を一つにまとめる。)
しかし前述した参考文章ではこんな例が出てきた。
(いつもnode.jsでコンソール結果を見るのでalert()
ではなくconsole.log()
に調整してます。)
class Thenable {
constructor(num) {
this.num = num
}
then(resolve, reject) {
console.log(resolve)
setTimeout(() => resolve(this.num * 2), 1000)
}
}
new Promise(resolve => resolve(1))
.then(result => {
return new Thenable(result)
})
.then(result => {
console.log(result)
})
// [Function (anonymous)] // function() { native code }
// 2
最初はプロミスのthen
で実行し、結果を受け取りnew Theable()
の引数として入れてreturn
する。ここから参考文章の説明を読んだうえの解釈ですが、return
した結果ではthen
メソッドがあったら再び中身の内容を実行し、この例ではconsole.log()
⇒ setTimeout()
⇒ resolve()
、
最後のresolve()
はPromise
のresolve()
のように引数(1 * 2)
を結果として下のthen
へ渡す。
then
の動きとしては
Promise
のthen
↓
class Thanable
のthen
(≒Promise
)
↓
Promise
のthen
変に感じたのは、class Thenable
のthen
は呼び出されてないし、return
もないのにどうやって結果を次のthen
へ渡すの?
そもそもclass Thenable
内部のthen
はPromise
のthen
と同じではないと感じていて、説明ではPromise
のようにresolve()
の引数が結果として下のthen
に渡せる。っていうことはclass
内部のthen
をthen
の名前を使って、実際にPromise
のような動きをしている...かな?
したがってこのプロミスチェーンの統合する特徴を利用すれば、class
内部で互換性のあるthen
メソッドを作って結果を返させ、利用することができるそうです。
2.3. コラム: Promiseは常に非同期?
2.7. コラム: thenは常に新しいpromiseオブジェクトを返す
Returning promises
同じくpromise chainingの例です。少し気になるところの感想メモです。
function loadScript(src) {
return new Promise(resolve, reject => {
let script = document.createElement('script')
script.src = src
script.onload = () => resolve(script)
script.onerror = () => reject(new Error(`Script load error: ${src}`))
document.head.append(script)
})
}
loadScript('/article/promise-chaining/one.js')
.then(script => {
return loadScript('/article/promise-chaining/two.js')
})
.then(script => {
return loadScript('/article/promise-chaining/three.js')
})
.then(script => {
one()
two()
three()
})
(これはただのデモコードです。実際に動けません。)
then
の動きが気になります。プロミスオブジェクトは最後のthen
に統合して一つまとめるそうです。上の例では、最後のthen
で三つのソースから関数を呼び出せるのは、then
チェーンでscript
で引数src
を貯蔵しているのかな?then
がreturn
したのは自分の引数script
ではなくloadScript
の結果です。そこから見ればプロミスオブジェクトには前の結果を上書きするのではなく、新しいsrc
を書き込まれ、最後のthen
に三つのsrc
から関数を呼び出せることになったのではないかと。
Error handling with promises
こちらのタスク問題です。
console.log('step 1')
new Promise(function promise(resolve, reject) {
console.log('step 2')
setTimeout(() => {
throw new Error('setTimeout Whoops!')
}, 1000)
// throw new Error('Whoops!')
resolve('Everything is OK')
}).catch(function catchError(err) {
console.log(err)
}).then(function getResult(result) {
console.log(result)
})
console.log('step 3')
// step 1
// step 2
// step 3
// Everything is OK
// Error: setTimeout Whoops! // global error
JavaScriptはsingle-thread、そしてプロミスは常に非同期に処理を行います。
JavaScriptがどうやってsingle-threadで非同期処理を実現するについて、前は少し同期と非同期処理の話題で触れていました。
今回はコードとツールを使って全体の動きを把握してみたいと思います。
JavaScript Visualizer 9000は非同期処理をビジュアル化したツールです。
今回デモコードの動きは こちら ご覧ください。
全体の動きとしては、
Call Stack
console.log('step 1')
↓
promise() {
console.log('step 2')
setTimeout((){}, 1000)
→ (Task Queue) anonymous
↓
console.log('step 3')
↓
prmise() {
resolve()
→ (Run all microtasks) calls getResult
}
↓
getResult(result) {
console.log(result)
// Everything is OK
}
(ここまでは主なコードが実行完了してCall Stackが綺麗になった状態で)
↓
setTimeout((){}, 1000)
1秒後
throw new Error('setTimeout Whoops!')
anonymous
を実行 // Error: setTimeout Whoops!
(このエラーはグローバルエラーになってpromise
のcatch()
を通さずJSエンジンがコンソールします。)
自分の考えですが、非同期処理といってもsingle-threadという制限のなか、非同期関数を一度別のキューに移行して下のコードに継続させて、主要なスタックが終わったら先に内部のMicrotask Queueを終わらせて結果を出します。それからTask Queueから残りの非同期関数を順番に終わらせる、といったステップを踏んでいろんな非同期を実現していると思います。
面白いと思うのはやはりresolve()
の動きです。自分の解釈ですが、resolve()
が呼び出されるとthen()
が連動し中の関数に引数を入れてすぐgetResult()
が呼び出されました。(簡単な概念かもしれませんが、resolve()
がどうやってthen
中の関数を呼び出すのがずっと気になっているので。)
補足:setTimeout
の計測器の触発は一回しかない、anonymous
は中身の関数に指しています。(setTimeout
の動きに興味がある方は こちら ご参考になれば幸いです。)
そしてエラーだった場合、処理がどう行われるかが こちら ご覧ください。
console.log('step 1')
new Promise(function promise(resolve, reject) {
console.log('step 2')
setTimeout(() => {
throw new Error('setTimeout Whoops!')
}, 1000)
throw new Error('Whoops!')
// resolve('Everything is OK')
}).catch(function catchError(err) {
console.log(err)
}).then(function getResult(result) {
console.log(result)
})
console.log('step 3')
// step 1
// step 2
// step 3
// Error: Whoops!
// undefined
// Error: setTimeout Whoops! // global error
ここの動きは、
console.log('step 1')
↓
promise() {
console.log('step 2')
setTimeout((){}, 1000)
→ (Task Queue) anonymous
throw new Error('Whoops!')
find error in 'promise', calls catchError
}
↓
console.log('step 3')
↓
catchError(err) {
console.log(err)
// Error: Whoops!
}
↓
getResult(result) {
console.log(result)
// undefined
}
↓
setTimeout((){}, 1000)
throw new Error('setTimeout Whoops!')
// Error: setTimeout Whoops!
エラーの場合はさきの例と動きが違いますね。promise()
でエラーをキャッチしたらすぐcatch
のcatchError()
へ連動してくれたけれど、コードが下に継続していきました。
さきの例と比べると私はこれも一種のError-Firstと考えていますが、なぜならresolve()
のような履行した状況と違い、エラーが出た場合はすぐ止めなきゃいけないからだと思います。エラーをキャッチしてすぐcatch()
へ移行したら中断になるけど、非同期の実現のためにcatch()
のcatchError
を実行しない、まずは外側のコードを完了してから、catchError()
実行しエラーメッセージをコンソール。それから後ろのチェーンを引き続き完了させる。
前の例ではresolve()
は明らかにconsole.log('step 3')
の後実行されgetResult
を呼び出した。これがpromise()
の中にエラーを発見していないから一旦外側のコードを完了させてまた戻る証拠だと思いますが。
別にエラーの検出によって全体の動きが大した変わらないし、まず外側のコードを完了するのが非同期の核心概念だと思いますが、プロミスの中の動きが工夫がなされていると感じています。