あなたがトップシンガーで、ファンがあなたの次の曲を昼夜問わず要求していると想像してください。
少しでも解放されるために、あなたは公開されたら彼らに送ると約束します。ファンにリストを渡します。彼らは自分のメールアドレスを記入することができ、曲が利用可能になったら、サブスクライブしたすべての関係者がすぐにそれを受け取ります。そして、スタジオで火災が発生し、曲を公開できなくなったとしても、彼らは通知を受け取ります。
誰もが幸せです:人々があなたに群がらなくなったためあなたも、そしてファンも曲を聞き逃さないからです。
これは、私たちがプログラミングでよく行うことの現実世界のアナロジーです。
- 何かをして時間がかかる「生成コード」。たとえば、ネットワーク経由でデータをロードするコード。それが「歌手」です。
- 準備が整ったら「生成コード」の結果を求める「消費コード」。多くの関数がその結果を必要とする場合があります。これらが「ファン」です。
- プロミスは、「生成コード」と「消費コード」をリンクする特別なJavaScriptオブジェクトです。私たちのアナロジーでは、これは「サブスクリプションリスト」です。「生成コード」は、約束された結果を生成するために必要な時間を費やし、「プロミス」は、準備が整ったときにサブスクライブされたすべてのコードがその結果を利用できるようにします。
JavaScriptのプロミスは単なるサブスクリプションリストよりも複雑であるため、このアナロジーはそれほど正確ではありません。追加の機能と制限があります。しかし、始めるにはこれで十分です。
プロミスオブジェクトのコンストラクタ構文は
let
promise =
new
Promise
(
function
(
resolve,
reject
)
{
// executor (the producing code, "singer")
}
)
;
new Promise
に渡される関数は、エグゼキュータと呼ばれます。new Promise
が作成されると、エグゼキュータが自動的に実行されます。これには、最終的に結果を生成するはずの生成コードが含まれています。上記のアナロジーでは、エグゼキュータは「歌手」です。
その引数であるresolve
とreject
は、JavaScript自体によって提供されるコールバックです。私たちのコードはエグゼキュータの中にのみあります。
エグゼキュータがすぐにまたは遅れて結果を取得した場合は、関係なく、これらのコールバックの1つを呼び出す必要があります。
resolve(value)
— ジョブが結果value
で正常に完了した場合。reject(error)
— エラーが発生した場合。error
はエラーオブジェクトです。
まとめると、エグゼキュータは自動的に実行され、ジョブの実行を試みます。試行が完了すると、成功した場合はresolve
を呼び出し、エラーが発生した場合はreject
を呼び出します。
new Promise
コンストラクタによって返されるpromise
オブジェクトには、次の内部プロパティがあります。
state
— 最初は"pending"
で、resolve
が呼び出された場合は"fulfilled"
に、reject
が呼び出された場合は"rejected"
に変わります。result
— 最初はundefined
で、resolve(value)
が呼び出された場合はvalue
に、reject(error)
が呼び出された場合はerror
に変わります。
したがって、エグゼキュータは最終的にpromise
を次のいずれかの状態に移動します。
後で、「ファン」がこれらの変更をサブスクライブする方法を見ていきます。
これは、プロミスコンストラクタと、時間がかかる(setTimeout
を使用)「生成コード」を含む単純なエグゼキュータ関数の例です。
let
promise =
new
Promise
(
function
(
resolve,
reject
)
{
// the function is executed automatically when the promise is constructed
// after 1 second signal that the job is done with the result "done"
setTimeout
(
(
)
=>
resolve
(
"done"
)
,
1000
)
;
}
)
;
上記のコードを実行すると、2つのことがわかります。
-
エグゼキュータは、(
new Promise
によって)自動的にすぐに呼び出されます。 -
エグゼキュータは2つの引数を受け取ります。
resolve
とreject
です。これらの関数はJavaScriptエンジンによって事前定義されているため、作成する必要はありません。準備が整ったら、それらの1つだけを呼び出す必要があります。1秒間の「処理」の後、エグゼキュータは
resolve("done")
を呼び出して結果を生成します。これにより、promise
オブジェクトの状態が変わります。
これは、ジョブが正常に完了した「履行されたプロミス」の例でした。
そして、これはエグゼキュータがエラーでプロミスを拒否する例です。
let
promise =
new
Promise
(
function
(
resolve,
reject
)
{
// after 1 second signal that the job is finished with an error
setTimeout
(
(
)
=>
reject
(
new
Error
(
"Whoops!"
)
)
,
1000
)
;
}
)
;
reject(...)
の呼び出しは、プロミスオブジェクトを"rejected"
状態に移動します。
まとめると、エグゼキュータはジョブ(通常は時間がかかるもの)を実行し、対応するプロミスオブジェクトの状態を変更するためにresolve
またはreject
を呼び出す必要があります。
解決済みまたは拒否済みのプロミスは、初期の「保留中」のプロミスとは対照的に、「確定」と呼ばれます。
エグゼキュータは、resolve
またはreject
のいずれか1つだけを呼び出す必要があります。状態の変更はすべて最終的なものです。
resolve
およびreject
のそれ以降の呼び出しはすべて無視されます。
let
promise =
new
Promise
(
function
(
resolve,
reject
)
{
resolve
(
"done"
)
;
reject
(
new
Error
(
"…"
)
)
;
// ignored
setTimeout
(
(
)
=>
resolve
(
"…"
)
)
;
// ignored
}
)
;
エグゼキュータによって実行されるジョブには、結果またはエラーが1つしかないという考えです。
また、resolve
/reject
は1つの引数(またはなし)のみを予期し、追加の引数は無視します。
Error
オブジェクトで拒否する何か問題が発生した場合、エグゼキュータはreject
を呼び出す必要があります。これは、(resolve
のように)任意の型の引数を使用して行うことができます。ただし、Error
オブジェクト(またはError
から継承するオブジェクト)を使用することをお勧めします。その理由はすぐに明らかになります。
resolve
/reject
をすぐに呼び出す実際には、エグゼキュータは通常、何かを非同期的に実行し、しばらくしてからresolve
/reject
を呼び出しますが、そうする必要はありません。このように、resolve
またはreject
をすぐに呼び出すこともできます。
let
promise =
new
Promise
(
function
(
resolve,
reject
)
{
// not taking our time to do the job
resolve
(
123
)
;
// immediately give the result: 123
}
)
;
たとえば、ジョブを開始したものの、すべてがすでに完了してキャッシュされていることがわかった場合に、このようなことが発生する可能性があります。
それで問題ありません。すぐに解決済みのプロミスが得られます。
state
とresult
は内部です。プロミスオブジェクトのプロパティstate
とresult
は内部です。直接アクセスすることはできません。そのためには、.then
/.catch
/.finally
メソッドを使用できます。それらについては以下で説明します。
コンシューマー: then、catch
プロミスオブジェクトは、エグゼキュータ(「生成コード」または「歌手」)と、結果またはエラーを受信する消費関数(「ファン」)との間のリンクとして機能します。消費関数は、.then
および.catch
メソッドを使用して登録(サブスクライブ)できます。
then
最も重要で基本的なものは.then
です。
構文は
promise.
then
(
function
(
result
)
{
/* handle a successful result */
}
,
function
(
error
)
{
/* handle an error */
}
)
;
.then
の最初の引数は、プロミスが解決されたときに実行され、結果を受け取る関数です。
.then
の2番目の引数は、プロミスが拒否されたときに実行され、エラーを受け取る関数です。
たとえば、正常に解決されたプロミスに対する反応を次に示します。
let
promise =
new
Promise
(
function
(
resolve,
reject
)
{
setTimeout
(
(
)
=>
resolve
(
"done!"
)
,
1000
)
;
}
)
;
// resolve runs the first function in .then
promise.
then
(
result
=>
alert
(
result)
,
// shows "done!" after 1 second
error
=>
alert
(
error)
// doesn't run
)
;
最初の関数が実行されました。
そして、拒否の場合、2番目の関数が実行されます。
let
promise =
new
Promise
(
function
(
resolve,
reject
)
{
setTimeout
(
(
)
=>
reject
(
new
Error
(
"Whoops!"
)
)
,
1000
)
;
}
)
;
// reject runs the second function in .then
promise.
then
(
result
=>
alert
(
result)
,
// doesn't run
error
=>
alert
(
error)
// shows "Error: Whoops!" after 1 second
)
;
成功した完了にのみ関心がある場合は、.then
に1つの関数引数のみを提供できます。
let
promise =
new
Promise
(
resolve
=>
{
setTimeout
(
(
)
=>
resolve
(
"done!"
)
,
1000
)
;
}
)
;
promise.
then
(
alert)
;
// shows "done!" after 1 second
catch
エラーのみに関心がある場合は、最初の引数としてnull
を使用できます: .then(null, errorHandlingFunction)
。または、.catch(errorHandlingFunction)
を使用できます。これはまったく同じです。
let
promise =
new
Promise
(
(
resolve,
reject
)
=>
{
setTimeout
(
(
)
=>
reject
(
new
Error
(
"Whoops!"
)
)
,
1000
)
;
}
)
;
// .catch(f) is the same as promise.then(null, f)
promise.
catch
(
alert)
;
// shows "Error: Whoops!" after 1 second
.catch(f)
の呼び出しは.then(null, f)
と完全に同じで、単なる省略形です。
クリーンアップ: finally
通常のtry {...} catch {...}
にfinally
句があるように、プロミスにもfinally
があります。
.finally(f)
の呼び出しは、プロミスが解決または拒否されたときに、常にf
が実行されるという意味で.then(f, f)
に似ています。
finally
の考え方は、前の操作が完了した後でクリーンアップ/ファイナライズを実行するためのハンドラを設定することです。
たとえば、読み込みインジケータの停止、不要になった接続のクローズなど。
パーティーのフィニッシャーと考えてください。パーティーが良かったか悪かったか、何人の友達がいたかに関係なく、私たちはまだ(少なくともそうするべきです)その後にクリーンアップをする必要があります。
コードは次のようになります。
new
Promise
(
(
resolve,
reject
)
=>
{
/* do something that takes time, and then call resolve or maybe reject */
}
)
// runs when the promise is settled, doesn't matter successfully or not
.
finally
(
(
)
=>
stop loading indicator)
// so the loading indicator is always stopped before we go on
.
then
(
result
=>
show result,
err
=>
show error)
finally(f)
はthen(f,f)
の正確なエイリアスではないことに注意してください。
重要な違いがあります。
-
finally
ハンドラには引数はありません。finally
では、プロミスが成功したか失敗したかわかりません。私たちのタスクは通常、「一般的な」ファイナライズ手順を実行することなので、それで問題ありません。上記の例を見てください。ご覧のとおり、
finally
ハンドラには引数がなく、プロミスの結果は次のハンドラによって処理されます。 -
finally
ハンドラは、結果またはエラーを次の適切なハンドラに「パススルー」します。たとえば、ここで結果は
finally
を介してthen
に渡されます。new
Promise
(
(
resolve
,
reject)
=>
{
setTimeout
(
(
)
=>
resolve
(
"value"
)
,
2000
)
;
}
)
.
finally
(
(
)
=>
alert
(
"Promise ready"
)
)
// triggers first
.
then
(
result
=>
alert
(
result)
)
;
// <-- .then shows "value"
ご覧のとおり、最初のPromiseによって返された
value
は、finally
を介して次のthen
に渡されます。これは非常に便利です。なぜなら、
finally
はPromiseの結果を処理するためのものではないからです。前述のとおり、これは結果がどうであれ、一般的なクリーンアップを行うための場所です。エラーがどのように
finally
を介してcatch
に渡されるかを示すために、エラーの例を次に示します。new
Promise
(
(
resolve
,
reject)
=>
{
throw
new
Error
(
"error"
)
;
}
)
.
finally
(
(
)
=>
alert
(
"Promise ready"
)
)
// triggers first
.
catch
(
err
=>
alert
(
err)
)
;
// <-- .catch shows the error
-
finally
ハンドラーも何も返す必要はありません。もし返した場合、返された値は黙って無視されます。このルールの唯一の例外は、
finally
ハンドラーがエラーをスローした場合です。この場合、このエラーは以前の結果の代わりに次のハンドラーに渡されます。
要約すると
finally
ハンドラーは、前のハンドラーの結果を取得しません(引数はありません)。この結果は代わりに、次の適切なハンドラーに渡されます。finally
ハンドラーが何かを返した場合、それは無視されます。finally
がエラーをスローすると、実行は最も近いエラーハンドラーに進みます。
これらの機能は役立ち、finally
を本来の使用方法である一般的なクリーンアップ手順に使用すると、物事が適切に機能するようになります。
Promiseが保留中の場合、.then/catch/finally
ハンドラーはその結果を待ちます。
場合によっては、ハンドラーを追加したときにPromiseがすでに確定していることがあります。
このような場合、これらのハンドラーはすぐに実行されます。
// the promise becomes resolved immediately upon creation
let
promise =
new
Promise
(
resolve
=>
resolve
(
"done!"
)
)
;
promise.
then
(
alert)
;
// done! (shows up right now)
これは、Promiseが現実世界の「サブスクリプションリスト」のシナリオよりも強力であることを示しています。もし歌手がすでに曲をリリースしていて、その後、ある人がサブスクリプションリストに登録した場合、おそらくその曲を受け取ることはできません。現実世界のサブスクリプションは、イベントが発生する前に完了する必要があります。
Promiseはより柔軟性があります。ハンドラーはいつでも追加できます。結果がすでに存在する場合、ハンドラーはただ実行されます。
例:loadScript
次に、Promiseが非同期コードの記述にどのように役立つかについて、より実践的な例を見ていきましょう。
前の章からスクリプトをロードするためのloadScript
関数があります。
コールバックベースのバリアントを思い出させるために、ここに示します。
function
loadScript
(
src,
callback
)
{
let
script =
document.
createElement
(
'script'
)
;
script.
src =
src;
script.
onload
=
(
)
=>
callback
(
null
,
script)
;
script.
onerror
=
(
)
=>
callback
(
new
Error
(
`
Script load error for
${
src}
`
)
)
;
document.
head.
append
(
script)
;
}
Promiseを使用してそれを書き直しましょう。
新しい関数loadScript
は、コールバックを必要としません。代わりに、ロードが完了したときに解決されるPromiseオブジェクトを作成して返します。外部コードは、.then
を使用してハンドラー(サブスクリプション関数)を追加できます。
function
loadScript
(
src
)
{
return
new
Promise
(
function
(
resolve,
reject
)
{
let
script =
document.
createElement
(
'script'
)
;
script.
src =
src;
script.
onload
=
(
)
=>
resolve
(
script)
;
script.
onerror
=
(
)
=>
reject
(
new
Error
(
`
Script load error for
${
src}
`
)
)
;
document.
head.
append
(
script)
;
}
)
;
}
使用法
let
promise =
loadScript
(
"https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.js"
)
;
promise.
then
(
script
=>
alert
(
`
${
script.
src}
is loaded!
`
)
,
error
=>
alert
(
`
Error:
${
error.
message}
`
)
)
;
promise.
then
(
script
=>
alert
(
'Another handler...'
)
)
;
コールバックベースのパターンと比較して、いくつかの利点をすぐに確認できます。
Promise | コールバック |
---|---|
Promiseを使用すると、自然な順序で処理を実行できます。最初にloadScript(script) を実行し、.then を使用して結果をどう処理するかを記述します。 |
loadScript(script, callback) を呼び出す際に、callback 関数を使用可能にする必要があります。言い換えれば、loadScript が呼び出される前に、結果をどう処理するかを知っておく必要があります。 |
Promiseで.then を必要な回数だけ呼び出すことができます。毎回、新しい「ファン」、つまり新しいサブスクリプション関数を「サブスクリプションリスト」に追加しています。これについては次の章「Promiseチェーン」で詳しく説明します。 |
コールバックは1つしか存在できません。 |
したがって、Promiseはコードの流れと柔軟性を向上させます。しかし、それだけではありません。それについては次の章で見ていきましょう。