本章では、Promiseによる非同期プログラミング全般、特にECMAScript 6のPromise APIについて入門します。前の章では、JavaScriptにおける非同期プログラミングの基礎を説明しています。本章で理解できない点があれば、そちらを参照してください。
then()
呼び出しのチェーン化fs.readFile()
のPromisifyXMLHttpRequest
のPromisifyPromise.resolve()
Promise.reject()
onRejected
からQを解決するthen()
を使用するPromise.all()
による計算のフォークと結合Promise.all()
によるmap()
Promise.race()
によるタイムアウトdone()
finally()
Promise
コンストラクタPromise
メソッドPromise.prototype
メソッドPromiseは、非同期計算の結果を配信するためのコールバックの代替手段です。非同期関数の作成者にはより多くの労力を要求しますが、それらの関数の使用者にはいくつかの利点を提供します。
次の関数は、Promiseを介して非同期的に結果を返します。
function
asyncFunc
()
{
return
new
Promise
(
function
(
resolve
,
reject
)
{
···
resolve
(
result
);
···
reject
(
error
);
});
}
asyncFunc()
は次のように呼び出します。
asyncFunc
()
.
then
(
result
=>
{
···
})
.
catch
(
error
=>
{
···
});
then()
呼び出しのチェーン化 then()
は常にPromiseを返し、メソッド呼び出しをチェーン化できます。
asyncFunc1
()
.
then
(
result1
=>
{
// Use result1
return
asyncFunction2
();
// (A)
})
.
then
(
result2
=>
{
// (B)
// Use result2
})
.
catch
(
error
=>
{
// Handle errors of asyncFunc1() and asyncFunc2()
});
then()
によって返されるPromise Pの解決方法は、そのコールバックが何をするかに依存します。
asyncFunction2
のPromiseの解決を受け取ることができます。さらに、catch()
が2つの非同期関数呼び出し(asyncFunction1()
とasyncFunction2()
)のエラーをどのように処理しているかに注目してください。つまり、キャッチされないエラーは、エラーハンドラが存在するまで渡されます。
then()
を介して非同期関数呼び出しをチェーン化する場合、それらは一度に一つずつ順番に実行されます。
asyncFunc1
()
.
then
(()
=>
asyncFunc2
());
そうせず、すべてをすぐに呼び出すと、基本的に並列に実行されます(Unixプロセスの用語ではフォーク)。
asyncFunc1
();
asyncFunc2
();
Promise.all()
を使用すると、すべての結果が揃った時点で通知を受け取ることができます(Unixプロセスの用語ではジョイン)。その入力はPromiseの配列であり、その出力は結果の配列で解決される単一のPromiseです。
Promise
.
all
([
asyncFunc1
(),
asyncFunc2
(),
])
.
then
(([
result1
,
result2
])
=>
{
···
})
.
catch
(
err
=>
{
// Receives first rejection among the Promises
···
});
Promise APIは、結果を非同期的に配信することについてです。Promiseオブジェクト(略してPromise)はそのオブジェクトを介して配信される結果の代役およびコンテナです。
状態
状態の変化への反応
then()
で登録するコールバックです。then()
メソッドを持つオブジェクトです。APIが解決の通知に関心しかない場合、Thenableのみを要求します(例:then()
とcatch()
から返される値、またはPromise.all()
とPromise.race()
に渡される値)。状態の変更:Promiseの状態を変更するための操作は2つあります。いずれか一方を一度呼び出した後、それ以上の呼び出しは無効になります。
Promiseは、特定の種類の非同期プログラミング、つまり単一の結果を非同期的に返す関数(またはメソッド)を処理するのに役立つパターンです。そのような結果を受け取る一般的な方法は、コールバックを使用することです(「継続としてのコールバック」)。
asyncFunction
(
arg1
,
arg2
,
result
=>
{
console
.
log
(
result
);
});
Promiseはコールバックを処理するより良い方法を提供します。非同期関数はPromise、つまり最終結果のプレースホルダーおよびコンテナとして機能するオブジェクトを返します。Promiseメソッドthen()
を介して登録されたコールバックは、結果を通知されます。
asyncFunction
(
arg1
,
arg2
)
.
then
(
result
=>
{
console
.
log
(
result
);
});
継続としてのコールバックと比較して、Promiseには次の利点があります。
then()
のコールバックがPromiseを返す場合(例:別のPromiseベースの関数を呼び出した結果)、then()
はそのPromiseを返します(これが実際にどのように機能するかはより複雑で、後で説明します)。その結果、then()
メソッド呼び出しをチェーン化できます。
asyncFunction1
(
a
,
b
)
.
then
(
result1
=>
{
console
.
log
(
result1
);
return
asyncFunction2
(
x
,
y
);
})
.
then
(
result2
=>
{
console
.
log
(
result2
);
});
Promiseの使用がどのようなものかを知るために、最初の例を見てみましょう。
Node.jsスタイルのコールバックを使用すると、ファイルの非同期読み取りは次のようになります。
fs
.
readFile
(
'config.json'
,
function
(
error
,
text
)
{
if
(
error
)
{
console
.
error
(
'Error while reading config file'
);
}
else
{
try
{
const
obj
=
JSON
.
parse
(
text
);
console
.
log
(
JSON
.
stringify
(
obj
,
null
,
4
));
}
catch
(
e
)
{
console
.
error
(
'Invalid JSON in file'
);
}
}
});
Promiseを使用すると、同じ機能は次のように使用されます。
readFilePromisified
(
'config.json'
)
.
then
(
function
(
text
)
{
// (A)
const
obj
=
JSON
.
parse
(
text
);
console
.
log
(
JSON
.
stringify
(
obj
,
null
,
4
));
})
.
catch
(
function
(
error
)
{
// (B)
// File read error or JSON SyntaxError
console
.
error
(
'An error occurred'
,
error
);
});
コールバックは依然として存在しますが、結果に対して呼び出されるメソッド(then()
とcatch()
)を介して提供されます。B行のエラーコールバックは2つの点で便利です。まず、エラー処理のスタイルが統一されている点(前の例のようにif (error)
やtry-catch
を使用する必要がない)。第二に、A行のコールバックとreadFilePromisified()
の両方のエラーを1箇所で処理できる点です。
readFilePromisified()
のコードは後述します。
Promiseを理解する3つの方法を見てみましょう。
次のコードは、Promiseベースの関数asyncFunc()
とその呼び出しを含んでいます。
function
asyncFunc
()
{
return
new
Promise
((
resolve
,
reject
)
=>
{
// (A)
setTimeout
(()
=>
resolve
(
'DONE'
),
100
);
// (B)
});
}
asyncFunc
()
.
then
(
x
=>
console
.
log
(
'Result: '
+
x
));
// Output:
// Result: DONE
asyncFunc()
はPromiseを返します。非同期計算の実際の結果'DONE'
の準備が整うと、A行で始まるコールバックのパラメータであるresolve()
(B行)を介して配信されます。
では、Promiseとは何でしょうか?
asyncFunc()
の呼び出しはブロッキング関数呼び出しです。次のコードは、非同期関数main()
からasyncFunc()
を呼び出します。非同期関数はECMAScript 2017の機能です。
async
function
main
()
{
const
x
=
await
asyncFunc
();
// (A)
console
.
log
(
'Result: '
+
x
);
// (B)
// Same as:
// asyncFunc()
// .then(x => console.log('Result: '+x));
}
main
();
main()
の本体は、何が起こっているかを概念的に、つまり非同期計算について私たちが通常どのように考えているかをうまく表現しています。つまり、asyncFunc()
はブロッキング関数呼び出しです。
asyncFunc()
が終了するまで待機します。x
を出力します。ECMAScript 6とジェネレータ以前は、コードを一時停止して再開することができませんでした。そのため、Promiseでは、コードが再開された後に発生するすべての処理をコールバックに記述します。そのコールバックを呼び出すことは、コードを再開することと同じです。
関数がPromiseを返す場合、そのPromiseは、関数が(通常は)計算が完了したら結果を書き込む空白のようなものです。このプロセスの簡単なバージョンを配列を使用してシミュレートできます。
function
asyncFunc
()
{
const
blank
=
[];
setTimeout
(()
=>
blank
.
push
(
'DONE'
),
100
);
return
blank
;
}
const
blank
=
asyncFunc
();
// Wait until the value has been filled in
setTimeout
(()
=>
{
const
x
=
blank
[
0
];
// (A)
console
.
log
(
'Result: '
+
x
);
},
200
);
Promiseでは、最終的な値に[0]
(A行のように)アクセスするのではなく、then()
メソッドとコールバックを使用します。
Promiseを別の見方として、イベントを発行するオブジェクトと考えることができます。
function
asyncFunc
()
{
const
eventEmitter
=
{
success
:
[]
};
setTimeout
(()
=>
{
// (A)
for
(
const
handler
of
eventEmitter
.
success
)
{
handler
(
'DONE'
);
}
},
100
);
return
eventEmitter
;
}
asyncFunc
()
.
success
.
push
(
x
=>
console
.
log
(
'Result: '
+
x
));
// (B)
イベントリスナー(B行)の登録は、asyncFunc()
を呼び出した後に行うことができます。これは、setTimeout()
に渡されたコールバック(A行)が非同期的に(このコードの部分が終了した後)実行されるためです。
通常のイベントエミッタは、登録されるとすぐに開始される複数のイベントの配信に特化しています。
一方、Promiseは正確に1つの値の配信に特化しており、登録が遅すぎることに対する組み込みの保護が備わっています。Promiseの結果はキャッシュされ、Promiseが確定した後に登録されたイベントリスナーに渡されます。
Promiseがプロデューサー側とコンシューマー側からどのように操作されるかを見てみましょう。
プロデューサーとして、Promiseを作成し、それを介して結果を送信します。
const
p
=
new
Promise
(
function
(
resolve
,
reject
)
{
// (A)
···
if
(
···
)
{
resolve
(
value
);
// success
}
else
{
reject
(
reason
);
// failure
}
});
Promiseを介して結果が配信されると、Promiseはその結果に固定されます。つまり、各Promiseは常に3つの(相互に排他的な)状態のいずれかになります。
Promiseは、fulfilledまたはrejectedのいずれかの場合に確定します(それが表す計算が完了しました)。Promiseは一度だけ確定し、その後は確定したままです。その後の確定の試みは効果がありません。
new Promise()
のパラメータ(A行から始まる)は、executorと呼ばれます。
resolve()
を介して結果を送信します。通常は、これによりPromise p
がfulfilledになります。しかし、そうとは限りません。Promise q
で解決すると、p
はq
を追跡することになります。q
がまだpendingの場合、p
もpendingです。しかし、q
が確定すると、p
も同じように確定します。reject()
を介してPromiseコンシューマに通知します。これにより、Promiseは常にrejectedになります。executor内で例外がスローされると、p
はその例外でrejectedになります。
promise
のコンシューマとして、反応(then()
とcatch()
メソッドで登録するコールバック)を介してfulfillmentまたはrejectionが通知されます。
promise
.
then
(
value
=>
{
/* fulfillment */
})
.
catch
(
error
=>
{
/* rejection */
});
Promiseが非同期関数(一発の結果を持つ関数)で非常に有用であるのは、Promiseが確定すると、それ以上変化しなくなるためです。さらに、Promiseが確定する前後にthen()
またはcatch()
を呼び出すかどうかに関係なく、競合状態は決して発生しません。
catch()
は、単にthen()
を呼び出すよりも便利(推奨)な代替手段であることに注意してください。つまり、次の2つの呼び出しは同等です。
promise
.
then
(
null
,
error
=>
{
/* rejection */
});
promise
.
catch
(
error
=>
{
/* rejection */
});
Promiseライブラリは、結果がPromiseの反応に同期的に(すぐに)配信されるか、非同期的に(現在の継続、現在のコードの部分が終了した後)配信されるかを完全に制御できます。ただし、Promises/A+仕様では、後者の実行モードを常に使用する必要があります。これは、次の要件(2.2.4)でthen()
メソッドについて規定されています。
onFulfilled
またはonRejected
は、実行コンテキストスタックにプラットフォームコードのみが含まれるまで呼び出されてはなりません。
つまり、コードは実行完了セマンティクス(前の章で説明)に依存でき、Promiseのチェーン化によって他のタスクの処理時間が不足することはありません。
さらに、この制約により、結果を同期的に返す場合と非同期的に返す場合がある関数を記述することが防止されます。これは、コードが予測不可能になるため、アンチパターンです。詳細については、Isaac Z. Schlueterによる「非同期のためのAPI設計」を参照してください。
Promiseを詳しく調べる前に、これまでの学習内容をいくつかの例で使用してみましょう。
fs.readFile()
のPromisify 次のコードは、組み込みのNode.js関数fs.readFile()
のPromiseベースのバージョンです。
import
{
readFile
}
from
'fs'
;
function
readFilePromisified
(
filename
)
{
return
new
Promise
(
function
(
resolve
,
reject
)
{
readFile
(
filename
,
{
encoding
:
'utf8'
},
(
error
,
data
)
=>
{
if
(
error
)
{
reject
(
error
);
}
else
{
resolve
(
data
);
}
});
});
}
readFilePromisified()
は次のように使用します。
readFilePromisified
(
process
.
argv
[
2
])
.
then
(
text
=>
{
console
.
log
(
text
);
})
.
catch
(
error
=>
{
console
.
log
(
error
);
});
XMLHttpRequest
のPromisify 以下は、イベントベースのXMLHttpRequest APIを介してHTTP GETを実行するPromiseベースの関数です。
function
httpGet
(
url
)
{
return
new
Promise
(
function
(
resolve
,
reject
)
{
const
request
=
new
XMLHttpRequest
();
request
.
onload
=
function
()
{
if
(
this
.
status
===
200
)
{
// Success
resolve
(
this
.
response
);
}
else
{
// Something went wrong (404 etc.)
reject
(
new
Error
(
this
.
statusText
));
}
};
request
.
onerror
=
function
()
{
reject
(
new
Error
(
'XMLHttpRequest Error: '
+
this
.
statusText
));
};
request
.
open
(
'GET'
,
url
);
request
.
send
();
});
}
httpGet()
は次のように使用します。
httpGet
(
'http://example.com/file.txt'
)
.
then
(
function
(
value
)
{
console
.
log
(
'Contents: '
+
value
);
},
function
(
reason
)
{
console
.
error
(
'Something went wrong'
,
reason
);
});
setTimeout()
をPromiseベースの関数delay()
として実装してみましょう(Q.delay()
に似ています)。
function
delay
(
ms
)
{
return
new
Promise
(
function
(
resolve
,
reject
)
{
setTimeout
(
resolve
,
ms
);
// (A)
});
}
// Using delay():
delay
(
5000
).
then
(
function
()
{
// (B)
console
.
log
(
'5 seconds have passed!'
)
});
A行では、パラメータなしでresolve
を呼び出していますが、これはresolve(undefined)
を呼び出すことと同じです。B行でもfulfillment値は必要なく、単に無視します。通知されるだけで十分です。
function
timeout
(
ms
,
promise
)
{
return
new
Promise
(
function
(
resolve
,
reject
)
{
promise
.
then
(
resolve
);
setTimeout
(
function
()
{
reject
(
new
Error
(
'Timeout after '
+
ms
+
' ms'
));
// (A)
},
ms
);
});
}
タイムアウト後の拒否(A行)はリクエストをキャンセルしませんが、Promiseがその結果でfulfilledされるのを防ぎます。
timeout()
は次のように使用します。
timeout
(
5000
,
httpGet
(
'http://example.com/file.txt'
))
.
then
(
function
(
value
)
{
console
.
log
(
'Contents: '
+
value
);
})
.
catch
(
function
(
reason
)
{
console
.
error
(
'Error or timeout'
,
reason
);
});
これで、Promiseの機能を詳しく調べることができます。まず、Promiseを作成するさらに2つの方法を探ってみましょう。
Promise.resolve()
Promise.resolve(x)
は次のように動作します。
x
に対して、x
でfulfilledされるPromiseを返します。
Promise
.
resolve
(
'abc'
)
.
then
(
x
=>
console
.
log
(
x
));
// abc
x
が、コンストラクタがレシーバであるPromise(Promise.resolve()
を呼び出す場合のPromise
)の場合、x
は変更されずに返されます。
const
p
=
new
Promise
(()
=>
null
);
console
.
log
(
Promise
.
resolve
(
p
)
===
p
);
// true
x
がthenableの場合、Promiseに変換されます。thenableの確定は、Promiseの確定にもなります。次のコードはそれを示しています。fulfilledThenable
は、文字列'hello'
でfulfilledされたPromiseとほぼ同じように動作します。それをPromise promise
に変換した後、then()
メソッドは期待通りに動作します(最終行)。
const
fulfilledThenable
=
{
then
(
reaction
)
{
reaction
(
'hello'
);
}
};
const
promise
=
Promise
.
resolve
(
fulfilledThenable
);
console
.
log
(
promise
instanceof
Promise
);
// true
promise
.
then
(
x
=>
console
.
log
(
x
));
// hello
つまり、Promise.resolve()
を使用して、任意の値(Promise、thenable、その他)をPromiseに変換できます。実際、これはPromise.all()
とPromise.race()
によって、任意の値の配列をPromiseの配列に変換するために使用されています。
Promise.reject()
Promise.reject(err)
は、err
で拒否された Promise を返します。
const
myError
=
new
Error
(
'Problem!'
);
Promise
.
reject
(
myError
)
.
catch
(
err
=>
console
.
log
(
err
===
myError
));
// true
このセクションでは、Promise のチェーン処理について詳しく見ていきます。メソッド呼び出しの結果は
P
.
then
(
onFulfilled
,
onRejected
)
新しい Promise Q です。つまり、Q に対して then()
を呼び出すことで、Promise ベースの制御フローを継続できます。
onFulfilled
または onRejected
が返す値で解決されます。onFulfilled
または onRejected
が例外をスローすると、Q は拒否されます。then()
が返す Promise Q を通常の値で解決する場合は、後続の then()
を介してその値を取得できます。
asyncFunc
()
.
then
(
function
(
value1
)
{
return
123
;
})
.
then
(
function
(
value2
)
{
console
.
log
(
value2
);
// 123
});
then()
が返す Promise Q を、thenable R で解決することもできます。thenable とは、Promise.prototype.then()
のように動作する then()
メソッドを持つオブジェクトです。したがって、Promise は thenable です。R で解決する(例:onFulfilled
から返す)ということは、Q の「後」に挿入されることを意味します。R の解決は、Q の onFulfilled
と onRejected
のコールバックに転送されます。ある意味、Q は R になります。
このメカニズムの主な用途は、次の例のようにネストされた then()
呼び出しをフラット化することです。
asyncFunc1
()
.
then
(
function
(
value1
)
{
asyncFunc2
()
.
then
(
function
(
value2
)
{
···
});
})
フラット版は次のようになります。
asyncFunc1
()
.
then
(
function
(
value1
)
{
return
asyncFunc2
();
})
.
then
(
function
(
value2
)
{
···
})
onRejected
から Q を解決する エラーハンドラーで返すものは何でも、解決値(拒否値ではない!)になります。これにより、失敗した場合に使用されるデフォルト値を指定できます。
retrieveFileName
()
.
catch
(
function
()
{
// Something went wrong, use a default value
return
'Untitled.txt'
;
})
.
then
(
function
(
fileName
)
{
···
});
then()
と catch()
のコールバックでスローされた例外は、拒否として次のエラーハンドラーに渡されます。
asyncFunc
()
.
then
(
function
(
value
)
{
throw
new
Error
();
})
.
catch
(
function
(
reason
)
{
// Handle error here
});
エラーハンドラーを持たない then()
メソッド呼び出しが1つ以上ある場合があります。その場合、エラーハンドラーがあるまでエラーが渡されます。
asyncFunc1
()
.
then
(
asyncFunc2
)
.
then
(
asyncFunc3
)
.
catch
(
function
(
reason
)
{
// Something went wrong above
});
次のコードでは、2つのPromiseのチェーンが構築されますが、その最初の部分のみが返されます。その結果、チェーンの末尾が失われます。
// Don’t do this
function
foo
()
{
const
promise
=
asyncFunc
();
promise
.
then
(
result
=>
{
···
});
return
promise
;
}
これは、チェーンの末尾を返すことで修正できます。
function
foo
()
{
const
promise
=
asyncFunc
();
return
promise
.
then
(
result
=>
{
···
});
}
変数promise
が必要ない場合は、このコードをさらに簡素化できます。
function
foo
()
{
return
asyncFunc
()
.
then
(
result
=>
{
···
});
}
次のコードでは、asyncFunc2()
の呼び出しがネストされています。
// Don’t do this
asyncFunc1
()
.
then
(
result1
=>
{
asyncFunc2
()
.
then
(
result2
=>
{
···
});
});
修正するには、最初の then()
から2番目の Promise を返し、2番目のチェーンされた then()
を介して処理します。
asyncFunc1
()
.
then
(
result1
=>
{
return
asyncFunc2
();
})
.
then
(
result2
=>
{
···
});
次のコードでは、メソッド insertInto()
はその結果に対して新しい Promise を作成します(A 行)。
// Don’t do this
class
Model
{
insertInto
(
db
)
{
return
new
Promise
((
resolve
,
reject
)
=>
{
// (A)
db
.
insert
(
this
.
fields
)
// (B)
.
then
(
resultCode
=>
{
this
.
notifyObservers
({
event
:
'created'
,
model
:
this
});
resolve
(
resultCode
);
// (C)
}).
catch
(
err
=>
{
reject
(
err
);
// (D)
})
});
}
···
}
よく見ると、結果の Promise は主に、非同期メソッド呼び出し db.insert()
(B 行)の解決(C 行)と拒否(D 行)を転送するために使用されていることがわかります。
修正するには、then()
を使用してチェーン処理することで、Promise を作成しません。
class
Model
{
insertInto
(
db
)
{
return
db
.
insert
(
this
.
fields
)
// (A)
.
then
(
resultCode
=>
{
this
.
notifyObservers
({
event
:
'created'
,
model
:
this
});
return
resultCode
;
// (B)
});
}
···
}
説明
resultCode
(B 行)を返し、then()
によって Promise を作成させます。then()
は db.insert()
によって生成された拒否を伝えます。then()
を使用する 原則として、catch(cb)
は then(null, cb)
の略記です。しかし、then()
の両方のパラメーターを同時に使用すると、問題が発生する可能性があります。
// Don’t do this
asyncFunc1
()
.
then
(
value
=>
{
// (A)
doSomething
();
// (B)
return
asyncFunc2
();
// (C)
},
error
=>
{
// (D)
···
});
拒否コールバック(D 行)は asyncFunc1()
のすべての拒否を受け取りますが、解決コールバック(A 行)によって作成された拒否は受け取りません。たとえば、B 行の同期関数呼び出しで例外がスローされるか、C 行の非同期関数呼び出しで拒否が発生する可能性があります。
したがって、拒否コールバックをチェーンされた catch()
に移動する方が良いでしょう。
asyncFunc1
()
.
then
(
value
=>
{
doSomething
();
return
asyncFunc2
();
})
.
catch
(
error
=>
{
···
});
プログラムには、2種類のエラーがあります。
運用エラーの場合、各関数はエラーをシグナルするための正確に1つの方法をサポートする必要があります。Promise ベースの関数の場合、それは拒否と例外を混在させないことを意味し、例外をスローしないことを意味します。
プログラマーエラーの場合、例外をスローすることで、できるだけ早く失敗することが理にかなっています。
function
downloadFile
(
url
)
{
if
(
typeof
url
!==
'string'
)
{
throw
new
Error
(
'Illegal argument: '
+
url
);
}
return
new
Promise
(
···
).
}
これを行う場合は、非同期コードが例外を処理できることを確認する必要があります。例外をスローすることは、理論的には静的にチェックできる(ソースコードを分析するリンターなどによる)アサーションや同様のものには許容できると考えています。
then()
と catch()
のコールバック内で例外がスローされた場合、これら2つのメソッドは例外を拒否に変換するため、問題ではありません。
しかし、同期処理を行うことで非同期関数を開始する場合、状況は異なります。
function
asyncFunc
()
{
doSomethingSync
();
// (A)
return
doSomethingAsync
()
.
then
(
result
=>
{
···
});
}
A 行で例外がスローされると、関数は例外をスローします。この問題には2つの解決策があります。
例外をキャッチして、拒否された Promise として返すことができます。
function
asyncFunc
()
{
try
{
doSomethingSync
();
return
doSomethingAsync
()
.
then
(
result
=>
{
···
});
}
catch
(
err
)
{
return
Promise
.
reject
(
err
);
}
}
Promise.resolve()
を介して then()
メソッド呼び出しのチェーンを開始し、コールバック内で同期コードを実行することもできます。
function
asyncFunc
()
{
return
Promise
.
resolve
()
.
then
(()
=>
{
doSomethingSync
();
return
doSomethingAsync
();
})
.
then
(
result
=>
{
···
});
}
別の方法として、Promise コンストラクターを介して Promise チェーンを開始できます。
function
asyncFunc
()
{
return
new
Promise
((
resolve
,
reject
)
=>
{
doSomethingSync
();
resolve
(
doSomethingAsync
());
})
.
then
(
result
=>
{
···
});
}
このアプローチにより、1ティック節約できます(同期コードはすぐに実行されます)が、コードの規則性が低下します。
このセクションの情報源
合成とは、既存の部分から新しいものを作成することです。Promise のシーケンシャルな合成はすでに経験済みです。2つの Promise P と Q がある場合、次のコードは P が解決された後に Q を実行する新しい Promise を生成します。
P
.
then
(()
=>
Q
)
これは、同期コードのセミコロンに似ていることに注意してください。同期操作 f()
と g()
のシーケンシャルな合成は次のようになります。
f
();
g
()
このセクションでは、Promise を合成する追加の方法について説明します。
2つの非同期計算、asyncFunc1()
と asyncFunc2()
を並行して実行するとします。
// Don’t do this
asyncFunc1
()
.
then
(
result1
=>
{
handleSuccess
({
result1
});
});
.
catch
(
handleError
);
asyncFunc2
()
.
then
(
result2
=>
{
handleSuccess
({
result2
});
})
.
catch
(
handleError
);
const
results
=
{};
function
handleSuccess
(
props
)
{
Object
.
assign
(
results
,
props
);
if
(
Object
.
keys
(
results
).
length
===
2
)
{
const
{
result1
,
result2
}
=
results
;
···
}
}
let
errorCounter
=
0
;
function
handleError
(
err
)
{
errorCounter
++
;
if
(
errorCounter
===
1
)
{
// One error means that everything failed,
// only react to first error
···
}
}
2つの関数呼び出し asyncFunc1()
と asyncFunc2()
は then()
チェーニングなしで行われます。その結果、どちらもすぐにほぼ並行して実行されます。実行はフォークされ、各関数呼び出しによって個別の「スレッド」が生成されます。両方のスレッドが終了すると(結果またはエラーで)、handleSuccess()
または handleError()
で単一のスレッドに結合されます。
このアプローチの問題は、手動でエラーが発生しやすい作業が多すぎることです。修正するには、組み込みメソッド Promise.all()
を使用して、自分で行わないことです。
Promise.all()
を使用した計算のフォークと結合 Promise.all(iterable)
は、Promiseのイテラブル(thenableおよびその他の値はPromise.resolve()
によってPromiseに変換されます)を受け取ります。それらのすべてがfulfilledされると、それらの値の配列でfulfilledされます。iterable
が空の場合、all()
によって返されるPromiseはすぐにfulfilledされます。
Promise
.
all
([
asyncFunc1
(),
asyncFunc2
(),
])
.
then
(([
result1
,
result2
])
=>
{
···
})
.
catch
(
err
=>
{
// Receives first rejection among the Promises
···
});
Promise.all()
によるmap()
Promiseの良い点の1つは、Promiseベースの関数は結果を返すため、多くの同期ツールが引き続き機能することです。たとえば、Arrayメソッドのmap()
を使用できます。
const
fileUrls
=
[
'http://example.com/file1.txt'
,
'http://example.com/file2.txt'
,
];
const
promisedTexts
=
fileUrls
.
map
(
httpGet
);
promisedTexts
はPromiseの配列です。前のセクションで既に説明したPromise.all()
を使用して、その配列を結果の配列でfulfilledされるPromiseに変換できます。
Promise
.
all
(
promisedTexts
)
.
then
(
texts
=>
{
for
(
const
text
of
texts
)
{
console
.
log
(
text
);
}
})
.
catch
(
reason
=>
{
// Receives first rejection among the Promises
});
Promise.race()
によるタイムアウト Promise.race(iterable)
は、Promiseのイテラブル(thenableおよびその他の値はPromise.resolve()
によってPromiseに変換されます)を受け取り、Promise Pを返します。入力Promiseの中で最初に解決されたものが、その解決状態を出力Promiseに渡します。iterable
が空の場合、race()
によって返されるPromiseは決して解決されません。
例として、タイムアウトを実装するためにPromise.race()
を使用してみましょう。
Promise
.
race
([
httpGet
(
'http://example.com/file.txt'
),
delay
(
5000
).
then
(
function
()
{
throw
new
Error
(
'Timed out'
)
});
])
.
then
(
function
(
text
)
{
···
})
.
catch
(
function
(
reason
)
{
···
});
このセクションでは、多くのPromiseライブラリが提供する、Promiseに関する2つの有用なメソッドについて説明します。これらはPromiseをさらに説明するためだけに示されており、Promise.prototype
に追加しないでください(この種の修正は、ポリフィルによってのみ行う必要があります)。
done()
複数のPromiseメソッド呼び出しをチェーンすると、エラーが暗黙的に破棄されるリスクがあります。たとえば
function
doSomething
()
{
asyncFunc
()
.
then
(
f1
)
.
catch
(
r1
)
.
then
(
f2
);
// (A)
}
A行のthen()
が拒否を生成した場合、どこでも処理されません。PromiseライブラリQは、メソッド呼び出しのチェーンの最後の要素として使用されるdone()
メソッドを提供します。これは最後のthen()
を置き換え、(1〜2個の引数を持つ)
function
doSomething
()
{
asyncFunc
()
.
then
(
f1
)
.
catch
(
r1
)
.
done
(
f2
);
}
または最後のthen()
の後に挿入されます(引数は0個)。
function
doSomething
()
{
asyncFunc
()
.
then
(
f1
)
.
catch
(
r1
)
.
then
(
f2
)
.
done
();
}
Qのドキュメントを引用すると
done
とthen
の使用に関する黄金律は、Promiseを他の誰かに返すか、チェーンが自分自身で終わる場合は、done
を呼び出して終了させることです。catch
で終了しても十分ではありません。なぜなら、catchハンドラー自体がエラーをスローする可能性があるからです。
ECMAScript 6でdone()
を実装する方法は次のとおりです。
Promise
.
prototype
.
done
=
function
(
onFulfilled
,
onRejected
)
{
this
.
then
(
onFulfilled
,
onRejected
)
.
catch
(
function
(
reason
)
{
// Throw an exception globally
setTimeout
(()
=>
{
throw
reason
},
0
);
});
};
done
の機能は明らかに有用ですが、ECMAScript 6には追加されていません。最初に、エンジンが自動的にどれだけ検出できるかを調査することが目的でした。それがどの程度うまく機能するかによって、done()
を導入する必要がある可能性があります。
finally()
エラーが発生したかどうかに関係なく、アクションを実行したい場合があります。たとえば、リソースの使用が終了した後にクリーンアップするためです。それがPromiseメソッドfinally()
の用途であり、例外処理のfinally
句と非常によく似ています。そのコールバックは引数を受け取りませんが、解決または拒否のいずれかが通知されます。
createResource
(
···
)
.
then
(
function
(
value1
)
{
// Use resource
})
.
then
(
function
(
value2
)
{
// Use resource
})
.
finally
(
function
()
{
// Clean up
});
Domenic Denicola
はこのようにfinally()
を実装することを提案しています。
Promise
.
prototype
.
finally
=
function
(
callback
)
{
const
P
=
this
.
constructor
;
// We don’t invoke the callback in here,
// because we want then() to handle its exceptions
return
this
.
then
(
// Callback fulfills => continue with receiver’s fulfillment or rejec\
tion
// Callback rejects => pass on that rejection (then() has no 2nd para\
meter
!
)
value
=>
P
.
resolve
(
callback
()).
then
(()
=>
value
),
reason
=>
P
.
resolve
(
callback
()).
then
(()
=>
{
throw
reason
})
);
};
コールバックは、レシーバ(this
)の解決方法を決定します。
finally()
によって返されるPromiseの解決になります。ある意味、メソッドのチェーンからfinally()
を取り除きます。例1(Jake Archibaldによる):スピナーを非表示にするためのfinally()
の使用。簡略版
showSpinner
();
fetchGalleryData
()
.
then
(
data
=>
updateGallery
(
data
))
.
catch
(
showNoDataError
)
.
finally
(
hideSpinner
);
例2(Kris Kowalによる):テストのティアダウンにfinally()
を使用。
const
HTTP
=
require
(
"q-io/http"
);
const
server
=
HTTP
.
Server
(
app
);
return
server
.
listen
(
0
)
.
then
(
function
()
{
// run test
})
.
finally
(
server
.
stop
);
PromiseライブラリQには、Node.jsスタイルの(err, result)
コールバックAPIとのインターフェースのためのツール関数があります。たとえば、denodeify
はコールバックベースの関数をPromiseベースの関数に変換します。
const
readFile
=
Q
.
denodeify
(
FS
.
readFile
);
readFile
(
'foo.txt'
,
'utf-8'
)
.
then
(
function
(
text
)
{
···
});
denodifyは、Q.denodeify()
の機能のみを提供し、ECMAScript 6 Promise APIに準拠するマイクロライブラリです。
多くのPromiseライブラリが存在します。次のものはECMAScript 6 APIに準拠しているため、現在使用でき、後でネイティブのES6に簡単に移行できます。
最小限のポリフィル
大規模なPromiseライブラリ
Q.Promise
はES6 APIを実装しています。ES6標準ライブラリのポリフィル
Promise
が含まれています。Promise
が含まれています。Promiseによる非同期関数の実装は、イベントまたはコールバックによる実装よりも便利ですが、それでも理想的ではありません。
解決策は、ブロッキングコールをJavaScriptに取り込むことです。ジェネレータを使用すると、ライブラリを介してこれを行うことができます。次のコードでは、制御フローライブラリcoを使用して、2つのJSONファイルを非同期的に取得します。
co
(
function
*
()
{
try
{
const
[
croftStr
,
bondStr
]
=
yield
Promise
.
all
([
// (A)
getFile
(
'http://localhost:8000/croft.json'
),
getFile
(
'http://localhost:8000/bond.json'
),
]);
const
croftJson
=
JSON
.
parse
(
croftStr
);
const
bondJson
=
JSON
.
parse
(
bondStr
);
console
.
log
(
croftJson
);
console
.
log
(
bondJson
);
}
catch
(
e
)
{
console
.
log
(
'Failure to read: '
+
e
);
}
});
A行では、Promise.all()
の結果が準備できるまで、yield
によって実行がブロック(待機)されます。つまり、非同期操作を実行しながら、コードは同期的に見えます。
ジェネレータに関する章で詳細を説明しています。
このセクションでは、別の角度からPromiseにアプローチします。APIの使用方法を学ぶのではなく、簡単な実装を見ていきます。この異なる角度は、Promiseを理解する上で非常に役立ちました。
Promiseの実装はDemoPromise
と呼ばれます。理解しやすくするために、APIと完全に一致するわけではありません。しかし、実際のインプリメンテーションが直面する課題について多くの洞察を与えるのに十分な近さです。
DemoPromise
は、3つのプロトタイプメソッドを持つクラスです。
DemoPromise.prototype.resolve(value)
DemoPromise.prototype.reject(reason)
DemoPromise.prototype.then(onFulfilled, onRejected)
つまり、resolve
とreject
はメソッドです(コンストラクタのコールバックパラメータに渡される関数ではありません)。
最初の実装は、最小限の機能を持つスタンドアロンのPromiseです。
then()
を介してリアクション(コールバック)を登録できます。Promiseが既に解決されているかどうかに関係なく、独立して機能する必要があります。この最初の実装の使用方法を次に示します。
const
dp
=
new
DemoPromise
();
dp
.
resolve
(
'abc'
);
dp
.
then
(
function
(
value
)
{
console
.
log
(
value
);
// abc
});
次の図は、最初の実装であるDemoPromise
の動作を示しています。
DemoPromise.prototype.then()
まずthen()
を調べましょう。2つのケースを処理する必要があります。
onFulfilled
とonRejected
の呼び出しをキューに入れます。onFulfilled
またはonRejected
をすぐに呼び出すことができます。
then
(
onFulfilled
,
onRejected
)
{
const
self
=
this
;
const
fulfilledTask
=
function
()
{
onFulfilled
(
self
.
promiseResult
);
};
const
rejectedTask
=
function
()
{
onRejected
(
self
.
promiseResult
);
};
switch
(
this
.
promiseState
)
{
case
'pending'
:
this
.
fulfillReactions
.
push
(
fulfilledTask
);
this
.
rejectReactions
.
push
(
rejectedTask
);
break
;
case
'fulfilled'
:
addToTaskQueue
(
fulfilledTask
);
break
;
case
'rejected'
:
addToTaskQueue
(
rejectedTask
);
break
;
}
}
前のコードスニペットは、次のヘルパー関数を使用します。
function
addToTaskQueue
(
task
)
{
setTimeout
(
task
,
0
);
}
DemoPromise.prototype.resolve()
resolve()
は次のように機能します。Promiseが既に解決されている場合、何も行いません(Promiseは一度だけ解決できることを保証します)。それ以外の場合は、Promiseの状態が'fulfilled'
に変更され、結果がthis.promiseResult
にキャッシュされます。次に、これまでにエンキューされたすべての履行リアクションがトリガーされます。
resolve
(
value
)
{
if
(
this
.
promiseState
!==
'pending'
)
return
;
this
.
promiseState
=
'fulfilled'
;
this
.
promiseResult
=
value
;
this
.
_clearAndEnqueueReactions
(
this
.
fulfillReactions
);
return
this
;
// enable chaining
}
_clearAndEnqueueReactions
(
reactions
)
{
this
.
fulfillReactions
=
undefined
;
this
.
rejectReactions
=
undefined
;
reactions
.
map
(
addToTaskQueue
);
}
reject()
はresolve()
に似ています。
次に実装する機能はチェーンです。
then()
は、onFulfilled
またはonRejected
が返すものを解決するPromiseを返します。onFulfilled
またはonRejected
がない場合、それらが受け取るものthen()
によって返されるPromiseに渡されます。明らかに、変更されるのはthen()
だけです。
then
(
onFulfilled
,
onRejected
)
{
const
returnValue
=
new
Promise
();
// (A)
const
self
=
this
;
let
fulfilledTask
;
if
(
typeof
onFulfilled
===
'function'
)
{
fulfilledTask
=
function
()
{
const
r
=
onFulfilled
(
self
.
promiseResult
);
returnValue
.
resolve
(
r
);
// (B)
};
}
else
{
fulfilledTask
=
function
()
{
returnValue
.
resolve
(
self
.
promiseResult
);
// (C)
};
}
let
rejectedTask
;
if
(
typeof
onRejected
===
'function'
)
{
rejectedTask
=
function
()
{
const
r
=
onRejected
(
self
.
promiseResult
);
returnValue
.
resolve
(
r
);
// (D)
};
}
else
{
rejectedTask
=
function
()
{
// `onRejected` has not been provided
// => we must pass on the rejection
returnValue
.
reject
(
self
.
promiseResult
);
// (E)
};
}
···
return
returnValue
;
// (F)
}
then()
は新しいPromiseを作成し、返します(A行とF行)。さらに、fulfilledTask
とrejectedTask
は異なる方法で設定されます。解決後…
onFulfilled
の結果を使用して、returnValue
を解決します(B行)。onFulfilled
がない場合は、履行値を使用してreturnValue
を解決します(C行)。onRejected
の結果を使用して、returnValue
を解決します(拒否しません!)(D行)。onRejected
がない場合は、拒否値をreturnValue
に渡します(E行)。フラット化は主に、チェーン処理をより便利にするためのものです。通常、リアクションから値を返すことは、それを次のthen()
に渡します。Promiseを返す場合、次の例のように「展開」されると便利です。
asyncFunc1
()
.
then
(
function
(
value1
)
{
return
asyncFunc2
();
// (A)
})
.
then
(
function
(
value2
)
{
// value2 is fulfillment value of asyncFunc2() Promise
console
.
log
(
value2
);
});
A行でPromiseを返し、現在のメソッド内にthen()
の呼び出しをネストする必要はありませんでした。メソッドの結果に対してthen()
を呼び出すことができました。したがって、ネストされたthen()
はなく、すべてフラットなままです。
これは、resolve()
メソッドにフラット化を行わせることで実装します。
Qを(Promiseだけでなく)thenableとして許可すると、フラット化をより汎用的にすることができます。
ロックインを実装するために、新しいブール型フラグthis.alreadyResolved
を導入します。これがtrueになると、this
はロックされ、それ以上解決できなくなります。その状態はロックインされているPromiseと同じであるため、this
は依然として保留中である可能性があることに注意してください。
resolve
(
value
)
{
if
(
this
.
alreadyResolved
)
return
;
this
.
alreadyResolved
=
true
;
this
.
_doResolve
(
value
);
return
this
;
// enable chaining
}
実際の解決は、プライベートメソッド_doResolve()
で行われます。
_doResolve
(
value
)
{
const
self
=
this
;
// Is `value` a thenable?
if
(
typeof
value
===
'object'
&&
value
!==
null
&&
'then'
in
value
)
{
// Forward fulfillments and rejections from `value` to `this`.
// Added as a task (versus done immediately) to preserve async semant\
ics
.
addToTaskQueue
(
function
()
{
// (A)
value
.
then
(
function
onFulfilled
(
result
)
{
self
.
_doResolve
(
result
);
},
function
onRejected
(
error
)
{
self
.
_doReject
(
error
);
});
});
}
else
{
this
.
promiseState
=
'fulfilled'
;
this
.
promiseResult
=
value
;
this
.
_clearAndEnqueueReactions
(
this
.
fulfillReactions
);
}
}
フラット化はA行で行われます。value
が履行された場合、self
を履行させたいですし、value
が拒否された場合、self
を拒否させたいです。alreadyResolved
による保護を回避するために、プライベートメソッド_doResolve
と_doReject
を介して転送が行われます。
チェーン処理を使用すると、Promiseの状態はより複雑になります(ECMAScript 6仕様の25.4節で説明されているとおり)。
Promiseを使用しているだけの場合は、通常、簡略化された世界観を採用し、ロックインを無視できます。最も重要な状態関連の概念は「解決済み」です。Promiseは、履行されているか拒否されているかのいずれかの場合に解決済みです。Promiseが解決済みになると、それ以上変更されません(状態と履行または拒否値)。
Promiseを実装する場合は、「解決」も重要であり、理解が難しくなります。
最後の機能として、ユーザーコードの例外を拒否として処理するPromiseを実装したいと思います。現時点では、「ユーザーコード」とは、then()
の2つのコールバックパラメーターを意味します。
次の抜粋は、A行でその呼び出しの周りにtry-catch
をラップすることにより、onFulfilled
内の例外を拒否に変換する方法を示しています。
then
(
onFulfilled
,
onRejected
)
{
···
let
fulfilledTask
;
if
(
typeof
onFulfilled
===
'function'
)
{
fulfilledTask
=
function
()
{
try
{
const
r
=
onFulfilled
(
self
.
promiseResult
);
// (A)
returnValue
.
resolve
(
r
);
}
catch
(
e
)
{
returnValue
.
reject
(
e
);
}
};
}
else
{
fulfilledTask
=
function
()
{
returnValue
.
resolve
(
self
.
promiseResult
);
};
}
···
}
DemoPromise
を実際のPromise実装に変換したい場合、公開コンストラクターパターン[2]も実装する必要があります。ES6 Promiseは、メソッドを介してではなく、コンストラクターのコールバックパラメーターであるexecutorに渡される関数によって解決および拒否されます。
executorが例外をスローした場合、「その」Promiseは拒否される必要があります。
Promiseの重要な利点の1つは、非同期ブラウザAPIでますます使用されるようになり、現在多様で互換性のないパターンと規約を統一することです。2つの今後のPromiseベースのAPIを見てみましょう。
Fetch APIは、XMLHttpRequestに代わるPromiseベースの代替手段です。
fetch
(
url
)
.
then
(
request
=>
request
.
text
())
.
then
(
str
=>
···
)
fetch()
は実際の要求に対するPromiseを返し、text()
は文字列としてコンテンツに対するPromiseを返します。
プログラムによるモジュールのインポートのためのECMAScript 6 APIもPromiseに基づいています。
System
.
import
(
'some_module.js'
)
.
then
(
some_module
=>
{
···
})
イベントと比較して、Promiseは一括結果の処理に適しています。結果を計算する前または後に結果に登録するかどうかは関係なく、結果を取得します。Promiseのこの利点は本質的に重要です。一方で、繰り返し発生するイベントの処理には使用できません。チェーン処理はPromiseのもう1つの利点ですが、イベント処理に追加できるものです。
コールバックと比較して、Promiseはよりクリーンな関数(またはメソッド)シグネチャを持っています。コールバックでは、パラメーターが入力と出力に使用されます。
fs
.
readFile
(
name
,
opts
?
,
(
err
,
string
|
Buffer
)
=>
void
)
Promiseでは、すべてのパラメーターが入力に使用されます。
readFilePromisified
(
name
,
opts
?
)
:
Promise
<
string
|
Buffer
>
その他のPromiseの利点としては、以下があります。
Array.prototype.map()
などの同期ツールを再利用できるため、合成が容易になります。then()
とcatch()
のチェーン処理。Promiseは、単一の非同期結果に適しています。以下には適していません。
ECMAScript 6 Promiseには、場合によっては便利な2つの機能がありません。
Q Promiseライブラリは後者に対するサポートがあり、Promises/A+に両方の機能を追加する計画があります。
このセクションでは、仕様で説明されているECMAScript 6 Promise APIの概要を示します。
Promise
コンストラクター Promiseのコンストラクターは、次のように呼び出されます。
const
p
=
new
Promise
(
function
(
resolve
,
reject
)
{
···
});
このコンストラクターのコールバックは、executorと呼ばれます。executorは、そのパラメーターを使用して新しいPromisep
を解決または拒否できます。
resolve(x)
はx
でp
を解決します。x
がthenableである場合、その解決はp
に転送されます(then()
を介して登録されたリアクションのトリガーが含まれます)。p
はx
で履行されます。reject(e)
は、値e
(多くの場合、Error
のインスタンス)でp
を拒否します。Promise
メソッド 次の2つの静的メソッドは、レシーバーの新しいインスタンスを作成します。
Promise.resolve(x)
:Promiseを認識して、任意の値をPromiseに変換します。x
のコンストラクターがレシーバーである場合、x
は変更されずに返されます。x
で履行されるレシーバーの新しいインスタンスを返します。Promise.reject(reason)
:値reason
で拒否されるレシーバーの新しいインスタンスを作成します。直感的には、静的メソッドPromise.all()
とPromise.race()
は、Promiseのiterableを単一のPromiseに合成します。つまり、
this.resolve()
を介してPromiseに変換されます。メソッドは以下のとおりです。
Promise.all(iterable)
:…なPromiseを返します。iterable
内のすべての要素が履行された場合に履行されます。Promise.race(iterable)
:解決されたiterable
の最初の要素を使用して、返されたPromiseが解決されます。Promise.prototype
メソッド Promise.prototype.then(onFulfilled, onRejected)
onFulfilled
とonRejected
は、リアクションと呼ばれます。onFulfilled
がすぐに呼び出されます。同様に、onRejected
はRejectionを通知されます。then()
は、新しいPromise Q(レシーバのコンストラクタの種別を使用して作成)を返します。onFulfilled
が省略されている場合、レシーバのFulfillmentはthen()
の結果に転送されます。onRejected
が省略されている場合、レシーバのRejectionはthen()
の結果に転送されます。省略されたリアクションのデフォルト値は、次のように実装できます。
function
defaultOnFulfilled
(
x
)
{
return
x
;
}
function
defaultOnRejected
(
e
)
{
throw
e
;
}
Promise.prototype.catch(onRejected)
p.catch(onRejected)
は、p.then(null, onRejected)
と同じです。[1] Brian CavalierとDomenic Denicolaによって編集された“Promises/A+”(JavaScript Promisesの事実上の標準)
[2] Domenic Denicolaによる“The Revealing Constructor Pattern”(このパターンはPromise
コンストラクタで使用されています)