.then()
.resolve()
.then()
呼び出しのチェーン.catch()
.then()
コールバックから返されたPromiseのフラット化.then()
のコールバックからPromiseを返す 必要な知識: Promise
この章では、Promiseについて大まかに理解している必要がありますが、関連する知識の多くはここで復習します。必要であれば、「JavaScript for impatient programmers」のPromiseに関する章を読むことができます。
この章では、Promiseに別の角度からアプローチします。このAPIを使用する代わりに、簡単な実装を作成します。この異なる角度は、かつて私がPromiseを理解するのに大いに役立ちました。
Promiseの実装は、クラスToyPromise
です。理解しやすくするために、APIとは完全に一致していません。しかし、Promiseの仕組みを理解するには十分に近いものです。
コードを含むリポジトリ
ToyPromise
は、GitHub の toy-promise
リポジトリで入手できます。
Promiseの状態の仕組みの簡略版から始めます(図11)
v
で*解決*されると、*履行済み*になります(後で、解決が拒否される場合もあることがわかります)。v
は、Promiseの*履行値*になります。e
で*拒否*されると、*拒否済み*になります。e
は、Promiseの*拒否値*になります。最初の実装は、最小限の機能を備えたスタンドアロンPromiseです。
.then()
を介して*反応*(コールバック)を登録できます。登録は、Promiseがすでに解決されているかどうかに関係なく、正しい処理を行う必要があります。.then()
はまだチェーンをサポートしていません。何も返しません。ToyPromise1
は、3つのプロトタイプメソッドを持つクラスです。
ToyPromise1.prototype.resolve(value)
ToyPromise1.prototype.reject(reason)
ToyPromise1.prototype.then(onFulfilled, onRejected)
つまり、resolve
と reject
はメソッドです(コンストラクターのコールバックパラメーターに渡される関数ではありません)。
これが最初の実装の使用方法です。
// .resolve() before .then()
const tp1 = new ToyPromise1();
tp1.resolve('abc');
tp1.then((value) => {
assert.equal(value, 'abc');
});
// .then() before .resolve()
const tp2 = new ToyPromise1();
tp2.then((value) => {
assert.equal(value, 'def');
});
tp2.resolve('def');
図12は、最初のToyPromise
の仕組みを示しています。
Promiseのデータフローの図はオプションです
図の目的は、Promiseの仕組みを視覚的に説明することです。しかし、それらはオプションです。混乱する場合は、無視してコードに集中できます。
ToyPromise1
: Promiseが解決されると、提供された値は*履行反応*(.then()
の最初の引数)に渡されます。Promiseが拒否されると、提供された値は*拒否反応*(.then()
の2番目の引数)に渡されます。.then()
最初に .then()
を調べてみましょう。2つのケースを処理する必要があります。
onFulfilled
と onRejected
の呼び出しをキューに入れます。これらは、Promiseが解決されたときに後で 使用されます。onFulfilled
または onRejected
をすぐに呼び出すことができます。then(onFulfilled, onRejected) {
const fulfillmentTask = () => {
if (typeof onFulfilled === 'function') {
onFulfilled(this._promiseResult);
}
};
const rejectionTask = () => {
if (typeof onRejected === 'function') {
onRejected(this._promiseResult);
}
};
switch (this._promiseState) {
case 'pending':
this._fulfillmentTasks.push(fulfillmentTask);
this._rejectionTasks.push(rejectionTask);
break;
case 'fulfilled':
addToTaskQueue(fulfillmentTask);
break;
case 'rejected':
addToTaskQueue(rejectionTask);
break;
default:
throw new Error();
}
}
前のコードスニペットでは、次のヘルパー関数を使用しています。
Promiseは常に非同期に解決される必要があります。そのため、タスクを直接実行するのではなく、イベントループ(ブラウザ、Node.jsなど)のタスクキューに追加します。実際のPromise APIは通常のタスク(setTimeout()
など)を使用せず、現在の通常のタスクと緊密に連携し、常にその直後に実行される*マイクロタスク*を使用することに注意してください。
.resolve()
.resolve()
は次のように動作します。Promiseがすでに解決されている場合、何も行いません(Promiseが一度だけ解決されるようにします)。そうでない場合、Promiseの状態は 'fulfilled'
に変わり、結果は this.promiseResult
にキャッシュされます。次に、これまでにエンキューされたすべての履行反応が呼び出されます。
resolve(value) {
if (this._promiseState !== 'pending') return this;
this._promiseState = 'fulfilled';
this._promiseResult = value;
this._clearAndEnqueueTasks(this._fulfillmentTasks);
return this; // enable chaining
}
_clearAndEnqueueTasks(tasks) {
this._fulfillmentTasks = undefined;
this._rejectionTasks = undefined;
tasks.map(addToTaskQueue);
}
reject()
は resolve()
に似ています。
.then()
呼び出しのチェーンToyPromise2
は .then()
呼び出しをチェーンします。.then()
は、履行反応または拒否反応によって返される値によって解決されるPromiseを返します。次に実装する機能はチェーンです(図13): 履行反応または拒否反応から返す値は、後続の .then()
呼び出しの履行反応で処理できます。(次のバージョンでは、Promiseを返すための特別なサポートにより、チェーンがはるかに便利になります。)
次の例では
.then()
: 履行反応で値を返します。.then()
: 履行反応を介してその値を受け取ります。new ToyPromise2()
.resolve('result1')
.then(x => {
assert.equal(x, 'result1');
return 'result2';
})
.then(x => {
assert.equal(x, 'result2');
});
次の例では
.then()
: 拒否反応で値を返します。.then()
: 履行反応を介してその値を受け取ります。new ToyPromise2()
.reject('error1')
.then(null,
x => {
assert.equal(x, 'error1');
return 'result2';
})
.then(x => {
assert.equal(x, 'result2');
});
.catch()
新しいバージョンでは、拒否反応のみを提供しやすくする簡便メソッド .catch()
が導入されています。履行反応のみを提供することはすでに簡単です。.then()
の2番目のパラメーターを省略するだけです(前の例を参照)。
前の例は、それを使用すると見栄えが良くなります(行A)。
new ToyPromise2()
.reject('error1')
.catch(x => { // (A)
assert.equal(x, 'error1');
return 'result2';
})
.then(x => {
assert.equal(x, 'result2');
});
次の2つのメソッド呼び出しは同等です。
これが .catch()
の実装方法です。
新しいバージョンでは、履行反応を省略すると履行が転送され、拒否反応を省略すると拒否が転送されます。なぜそれが役に立つのでしょうか?
次の例は、拒否の受け渡しを示しています。
someAsyncFunction()
.then(fulfillmentReaction1)
.then(fulfillmentReaction2)
.catch(rejectionReaction);
rejectionReaction
は、someAsyncFunction()
、fulfillmentReaction1
、および fulfillmentReaction2
の拒否を処理できるようになりました。
次の例は、履行の受け渡しを示しています。
someAsyncFunction()
がPromiseを拒否した場合、rejectionReaction
は問題を修正し、fulfillmentReaction
によって処理される履行値を返すことができます。
someAsyncFunction()
がPromiseを履行した場合、.catch()
がスキップされるため、fulfillmentReaction
もそれを処理できます。
これはすべてどのように内部で処理されるのでしょうか?
.then()
は、onFulfilled
または onRejected
が返すもので解決されるPromiseを返します。onFulfilled
または onRejected
がない場合、それらが受け取るものは何でも .then()
によって返されるPromiseに渡されます。.then()
のみが変更されます。
then(onFulfilled, onRejected) {
const resultPromise = new ToyPromise2(); // [new]
const fulfillmentTask = () => {
if (typeof onFulfilled === 'function') {
const returned = onFulfilled(this._promiseResult);
resultPromise.resolve(returned); // [new]
} else { // [new]
// `onFulfilled` is missing
// => we must pass on the fulfillment value
resultPromise.resolve(this._promiseResult);
}
};
const rejectionTask = () => {
if (typeof onRejected === 'function') {
const returned = onRejected(this._promiseResult);
resultPromise.resolve(returned); // [new]
} else { // [new]
// `onRejected` is missing
// => we must pass on the rejection value
resultPromise.reject(this._promiseResult);
}
};
···
return resultPromise; // [new]
}
.then()
は新しいPromiseを作成して返します(メソッドの最初の行と最後の行)。さらに
fulfillmentTask
は異なって動作します。これが履行後に発生することです。onFullfilled
が提供された場合、それが呼び出され、その結果が resultPromise
を解決するために使用されます。onFulfilled
がない場合、現在のPromiseの履行値を使用して resultPromise
を解決します。rejectionTask
は異なって動作します。これが拒否後に発生することです。onRejected
が提供された場合、それが呼び出され、その結果が resultPromise
を*解決*するために使用されます。resultPromise
は拒否されないことに注意してください。onRejected()
が問題を修正したと想定しています。onRejected
がない場合、現在のPromiseの拒否値を使用して resultPromise
を拒否します。.then()
コールバックから返されたPromiseのフラット化.then()
のコールバックからPromiseを返すPromiseのフラット化は、主にチェーンをより便利にすることです。ある .then()
コールバックから次のコールバックに値を渡したい場合は、前者でそれを返します。その後、.then()
はそれをすでに返したPromiseに入れます。
.then()
コールバックからPromiseを返すと、このアプローチは不便になります。たとえば、Promiseベース関数の結果(行A)
asyncFunc1()
.then((result1) => {
assert.equal(result1, 'Result of asyncFunc1()');
return asyncFunc2(); // (A)
})
.then((result2Promise) => {
result2Promise
.then((result2) => { // (B)
assert.equal(
result2, 'Result of asyncFunc2()');
});
});
今回、A行で返された値を .then()
によって返されたPromiseに入れると、B行でそのPromiseをアンラップする必要があります。代わりに、A行で返されたPromiseが .then()
によって返されたPromiseを置き換えればよいでしょう。それがどのように行われるかはすぐにはわかりませんが、それが機能すれば、次のようにコードを書くことができます。
asyncFunc1()
.then((result1) => {
assert.equal(result1, 'Result of asyncFunc1()');
return asyncFunc2(); // (A)
})
.then((result2) => {
// result2 is the fulfillment value, not the Promise
assert.equal(
result2, 'Result of asyncFunc2()');
});
A行では、Promiseを返しました。Promiseのフラット化のおかげで、result2
はそのPromiseの履行値であり、Promise自体ではありません。
ECMAScript仕様におけるPromiseのフラット化
ECMAScript仕様では、Promiseのフラット化の詳細は、セクション「Promiseオブジェクト」で説明されています。
Promise APIはフラット化をどのように処理するのでしょうか?
Promise PがPromise Qで解決された場合、PはQをラップせず、PはQに「なります」: Pの状態と解決値は常にQと同じになります。これは、.then()
がコールバックの1つによって返された値で返すPromiseを解決するため、.then()
に役立ちます。
PはどのようにQになるのでしょうか? Qに*ロックイン*することによって: Pは外部から解決できなくなり、Qの解決がPの解決をトリガーします。ロックインは、状態をより複雑にする追加の不可視のPromise状態です。
Promise API には、Q が Promise である必要はなく、いわゆる *thenable* であれば良いという、Q に関して追加の機能があります。 thenable とは、`then()` メソッドを持つオブジェクトです。この柔軟性が追加された理由は、異なる Promise 実装が連携できるようにするためです(これは、Promise が最初に言語に追加されたときに重要でした)。
図 14 は、新しい状態を視覚化したものです。
*解決* の概念もより複雑になっていることに注意してください。 Promise を解決するということは、もはや直接決済できないということを意味するようになりました。
ECMAScript 仕様では、次のように述べられています。「未解決の Promise は常に保留状態です。解決された Promise は、保留、履行、または拒否される可能性があります。」
図 15 は、`ToyPromise3` がフラット化をどのように処理するかを示しています。
thenable は、次の関数を使用して検出します。
function isThenable(value) { // [new]
return typeof value === 'object' && value !== null
&& typeof value.then === 'function';
}
ロックインを実装するために、新しいブール値フラグ `._alreadyResolved` を導入します。これを `true` に設定すると、`.resolve()` と `.reject()` が無効になります。例えば、
resolve(value) { // [new]
if (this._alreadyResolved) return this;
this._alreadyResolved = true;
if (isThenable(value)) {
// Forward fulfillments and rejections from `value` to `this`.
// The callbacks are always executed asynchronously
value.then(
(result) => this._doFulfill(result),
(error) => this._doReject(error));
} else {
this._doFulfill(value);
}
return this; // enable chaining
}
`value` が thenable の場合、現在の Promise をそれにロックインします。
決済は、`._alreadyResolved` による保護を回避するために、プライベートメソッド `._doFulfill()` と `._doReject()` を介して実行されます。
`._doFulfill()` は比較的単純です。
_doFulfill(value) { // [new]
assert.ok(!isThenable(value));
this._promiseState = 'fulfilled';
this._promiseResult = value;
this._clearAndEnqueueTasks(this._fulfillmentTasks);
}
`.reject()` はここでは示されていません。その唯一の新しい機能は、`._alreadyResolved` にも従うようになったことです。
最後の機能として、Promise がユーザーコードの例外を拒否として処理するようにします(図 16)。この章では、「ユーザーコード」とは `.then()` の 2 つのコールバックパラメータを意味します。
new ToyPromise4()
.resolve('a')
.then((value) => {
assert.equal(value, 'a');
throw 'b'; // triggers a rejection
})
.catch((error) => {
assert.equal(error, 'b');
})
`.then()` は、ヘルパーメソッド `._runReactionSafely()` を介して、Promise リアクション `onFulfilled` と `onRejected` を安全に実行するようになりました。例えば、
const fulfillmentTask = () => {
if (typeof onFulfilled === 'function') {
this._runReactionSafely(resultPromise, onFulfilled); // [new]
} else {
// `onFulfilled` is missing
// => we must pass on the fulfillment value
resultPromise.resolve(this._promiseResult);
}
};
`._runReactionSafely()` は次のように実装されます。
_runReactionSafely(resultPromise, reaction) { // [new]
try {
const returned = reaction(this._promiseResult);
resultPromise.resolve(returned);
} catch (e) {
resultPromise.reject(e);
}
}
最後の手順を省略しています。`ToyPromise` を実際の Promise 実装に変換する場合、公開コンストラクターパターンを実装する必要があります。JavaScript の Promise は、メソッドを介して解決および拒否されるのではなく、コンストラクターのコールバックパラメーターである *executor* に渡される関数を介して解決および拒否されます。
executor が例外をスローした場合、`promise` は拒否されます。