初めに
プロミスでは約束した書き方など、統一されたやり方で従う点がたくさんあるので、構造上の話ではなく、使い方や動きから思ったことをまとめていきたいと思います。
今回主な参考文章はこちらです。
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()の中にエラーを発見していないから一旦外側のコードを完了させてまた戻る証拠だと思いますが。
別にエラーの検出によって全体の動きが大した変わらないし、まず外側のコードを完了するのが非同期の核心概念だと思いますが、プロミスの中の動きが工夫がなされていると感じています。