はじめに: コールバックの章で述べた問題に戻りましょう。一連の非同期タスクを順番に実行する必要があります。たとえば、スクリプトの読み込みなどです。どのようにコーディングすればよいでしょうか?
Promiseには、それを行うためのいくつかの方法があります。
この章では、プロミスのチェーンについて説明します。
次のようになります。
new Promise(function(resolve, reject) {
setTimeout(() => resolve(1), 1000); // (*)
}).then(function(result) { // (**)
alert(result); // 1
return result * 2;
}).then(function(result) { // (***)
alert(result); // 2
return result * 2;
}).then(function(result) {
alert(result); // 4
return result * 2;
});
基本的な考え方は、結果が.then
ハンドラのチェーンを通過することです。
ここでの流れは次のとおりです。
- 最初のPromiseは1秒後に解決し
(*)
、 - 次に、
.then
ハンドラが呼び出され(**)
、新しいPromise(値2
で解決される)が作成されます。 - 次の
then
(***)
は、前の結果を受け取り、処理(2倍にする)して、次のハンドラに渡します。 - …など。
結果がハンドラのチェーンに沿って渡されるため、alert
の呼び出しが順番に表示されます:1
→ 2
→ 4
。
すべてが機能するのは、.then
を呼び出すたびに新しいPromiseが返されるため、その後に次の.then
を呼び出すことができるからです。
ハンドラが値を返すとき、それはそのPromiseの結果となり、次の.then
がその値で呼び出されます。
よくある初心者のエラー: 技術的には、単一のPromiseに多くの.then
を追加することもできます。これはチェーンではありません。
例:
let promise = new Promise(function(resolve, reject) {
setTimeout(() => resolve(1), 1000);
});
promise.then(function(result) {
alert(result); // 1
return result * 2;
});
promise.then(function(result) {
alert(result); // 1
return result * 2;
});
promise.then(function(result) {
alert(result); // 1
return result * 2;
});
ここでやったことは、1つのPromiseにいくつかのハンドラを追加しただけです。それらは結果を互いに渡すのではなく、独立して処理します。
これが図です(上記のチェーンと比較してください)
同じPromiseのすべての.then
は、同じ結果、つまりそのPromiseの結果を受け取ります。したがって、上記のコードでは、すべてのalert
が同じものを表示します:1
。
実際には、1つのPromiseに対して複数のハンドラが必要になることはめったにありません。チェーンがはるかに頻繁に使用されます。
Promiseの返却
.then(handler)
で使用されるハンドラは、Promiseを作成して返すことができます。
その場合、さらに後続のハンドラはそれが確定するまで待ち、その結果を取得します。
例:
new Promise(function(resolve, reject) {
setTimeout(() => resolve(1), 1000);
}).then(function(result) {
alert(result); // 1
return new Promise((resolve, reject) => { // (*)
setTimeout(() => resolve(result * 2), 1000);
});
}).then(function(result) { // (**)
alert(result); // 2
return new Promise((resolve, reject) => {
setTimeout(() => resolve(result * 2), 1000);
});
}).then(function(result) {
alert(result); // 4
});
ここで最初の.then
は1
を表示し、(*)
の行でnew Promise(…)
を返します。1秒後、それは解決され、結果(resolve
の引数で、ここではresult * 2
)が2番目の.then
のハンドラに渡されます。そのハンドラは(**)
の行にあり、2
を表示し、同じことを行います。
したがって、出力は前の例と同じ:1 → 2 → 4ですが、今度はalert
の呼び出しの間に1秒の遅延があります。
Promiseを返すことで、非同期アクションのチェーンを構築できます。
例: loadScript
前の章で定義したPromise化されたloadScript
で、この機能を使用して、スクリプトを順番に一つずつロードしましょう。
loadScript("/article/promise-chaining/one.js")
.then(function(script) {
return loadScript("/article/promise-chaining/two.js");
})
.then(function(script) {
return loadScript("/article/promise-chaining/three.js");
})
.then(function(script) {
// use functions declared in scripts
// to show that they indeed loaded
one();
two();
three();
});
このコードは、アロー関数を使用して少し短くすることができます。
loadScript("/article/promise-chaining/one.js")
.then(script => loadScript("/article/promise-chaining/two.js"))
.then(script => loadScript("/article/promise-chaining/three.js"))
.then(script => {
// scripts are loaded, we can use functions declared there
one();
two();
three();
});
ここで、各loadScript
の呼び出しはPromiseを返し、それが解決されると次の.then
が実行されます。次に、次のスクリプトの読み込みを開始します。したがって、スクリプトは次々とロードされます。
チェーンにさらに非同期アクションを追加できます。コードは依然として「フラット」であることに注意してください。それは右ではなく下に伸びます。「破滅のピラミッド」の兆候はありません。
技術的には、次のように各loadScript
に直接.then
を追加することもできます。
loadScript("/article/promise-chaining/one.js").then(script1 => {
loadScript("/article/promise-chaining/two.js").then(script2 => {
loadScript("/article/promise-chaining/three.js").then(script3 => {
// this function has access to variables script1, script2 and script3
one();
two();
three();
});
});
});
このコードは同じことを行います:3つのスクリプトを順番にロードします。しかし、それは「右に伸びます」。したがって、コールバックと同じ問題があります。
Promiseを使い始めた人は、チェーンについて知らないことがあるため、このように記述します。一般的に、チェーンが推奨されます。
入れ子になった関数が外側のスコープにアクセスできるため、.then
を直接記述しても問題ない場合があります。上記の例では、最もネストされたコールバックは、すべての変数script1
、script2
、script3
にアクセスできます。ただし、これはルールではなく例外です。
厳密に言うと、ハンドラは厳密にPromiseではなく、いわゆる「thenable」オブジェクト、つまり.then
メソッドを持つ任意のオブジェクトを返す可能性があります。これはPromiseと同じように扱われます。
その考え方は、サードパーティのライブラリが独自の「Promise互換」オブジェクトを実装できるということです。それらは拡張されたメソッドのセットを持つことができますが、.then
を実装するため、ネイティブPromiseとも互換性があります。
これはthenableオブジェクトの例です。
class Thenable {
constructor(num) {
this.num = num;
}
then(resolve, reject) {
alert(resolve); // function() { native code }
// resolve with this.num*2 after the 1 second
setTimeout(() => resolve(this.num * 2), 1000); // (**)
}
}
new Promise(resolve => resolve(1))
.then(result => {
return new Thenable(result); // (*)
})
.then(alert); // shows 2 after 1000ms
JavaScriptは、(*)
の行の.then
ハンドラによって返されたオブジェクトをチェックします。もし呼び出し可能なthen
という名前のメソッドがある場合、ネイティブ関数resolve
、reject
を引数として(executorと同様に)提供してそのメソッドを呼び出し、それらのいずれかが呼び出されるまで待ちます。上記の例では、(**)
で1秒後にresolve(2)
が呼び出されます。その後、結果はチェーンをさらに下に渡されます。
この機能により、Promise
から継承することなく、カスタムオブジェクトをPromiseチェーンと統合できます。
より大きな例:fetch
フロントエンドプログラミングでは、Promiseはネットワークリクエストによく使用されます。したがって、その拡張例を見てみましょう。
fetchメソッドを使用して、リモートサーバーからユーザー情報をロードします。別の章で説明されている多くのオプションパラメータがありますが、基本的な構文は非常に簡単です。
let promise = fetch(url);
これにより、url
へのネットワークリクエストが行われ、Promiseが返されます。Promiseは、リモートサーバーがヘッダーで応答したときに、完全な応答がダウンロードされる前に、response
オブジェクトで解決されます。
完全な応答を読み取るには、メソッドresponse.text()
を呼び出す必要があります。これにより、リモートサーバーから完全なテキストがダウンロードされると、そのテキストを結果として解決するPromiseが返されます。
次のコードは、user.json
にリクエストを行い、サーバーからそのテキストをロードします。
fetch('/article/promise-chaining/user.json')
// .then below runs when the remote server responds
.then(function(response) {
// response.text() returns a new promise that resolves with the full response text
// when it loads
return response.text();
})
.then(function(text) {
// ...and here's the content of the remote file
alert(text); // {"name": "iliakan", "isAdmin": true}
});
fetch
から返されるresponse
オブジェクトには、リモートデータを読み取り、JSONとして解析するメソッドresponse.json()
も含まれています。私たちの場合はさらに便利なので、それに切り替えましょう。
簡潔にするために、アロー関数も使用します。
// same as above, but response.json() parses the remote content as JSON
fetch('/article/promise-chaining/user.json')
.then(response => response.json())
.then(user => alert(user.name)); // iliakan, got user name
ロードされたユーザーで何かをしてみましょう。
たとえば、GitHubにさらにリクエストを行い、ユーザープロファイルをロードしてアバターを表示することができます。
// Make a request for user.json
fetch('/article/promise-chaining/user.json')
// Load it as json
.then(response => response.json())
// Make a request to GitHub
.then(user => fetch(`https://api.github.com/users/${user.name}`))
// Load the response as json
.then(response => response.json())
// Show the avatar image (githubUser.avatar_url) for 3 seconds (maybe animate it)
.then(githubUser => {
let img = document.createElement('img');
img.src = githubUser.avatar_url;
img.className = "promise-avatar-example";
document.body.append(img);
setTimeout(() => img.remove(), 3000); // (*)
});
コードは機能します。詳細についてはコメントを参照してください。ただし、それには潜在的な問題があり、Promiseを使い始めた人の典型的なエラーです。
(*)
の行を見てください。アバターの表示が終了して削除された後に何かをするにはどうすればよいでしょうか?たとえば、そのユーザーを編集するためのフォームなどを表示したいと思います。今のところ、方法はありません。
チェーンを拡張可能にするには、アバターの表示が終了したときに解決されるPromiseを返す必要があります。
こんな感じです。
fetch('/article/promise-chaining/user.json')
.then(response => response.json())
.then(user => fetch(`https://api.github.com/users/${user.name}`))
.then(response => response.json())
.then(githubUser => new Promise(function(resolve, reject) { // (*)
let img = document.createElement('img');
img.src = githubUser.avatar_url;
img.className = "promise-avatar-example";
document.body.append(img);
setTimeout(() => {
img.remove();
resolve(githubUser); // (**)
}, 3000);
}))
// triggers after 3 seconds
.then(githubUser => alert(`Finished showing ${githubUser.name}`));
つまり、(*)
の行の.then
ハンドラは、setTimeout
(**)
のresolve(githubUser)
の呼び出し後にのみ確定するnew Promise
を返します。チェーン内の次の.then
はそれを待ちます。
良い習慣として、非同期アクションは常にPromiseを返す必要があります。これにより、その後にアクションを計画することが可能になります。今すぐチェーンを拡張する予定がない場合でも、後で必要になる可能性があります。
最後に、コードを再利用可能な関数に分割できます。
function loadJson(url) {
return fetch(url)
.then(response => response.json());
}
function loadGithubUser(name) {
return loadJson(`https://api.github.com/users/${name}`);
}
function showAvatar(githubUser) {
return new Promise(function(resolve, reject) {
let img = document.createElement('img');
img.src = githubUser.avatar_url;
img.className = "promise-avatar-example";
document.body.append(img);
setTimeout(() => {
img.remove();
resolve(githubUser);
}, 3000);
});
}
// Use them:
loadJson('/article/promise-chaining/user.json')
.then(user => loadGithubUser(user.name))
.then(showAvatar)
.then(githubUser => alert(`Finished showing ${githubUser.name}`));
// ...
まとめ
.then
(またはcatch/finally
。関係ありません)ハンドラがPromiseを返す場合、チェーンの残りの部分はそれが確定するまで待ちます。そうすると、その結果(またはエラー)がさらに渡されます。
これが全体像です。
コメント
<code>
タグを使用し、数行の場合は<pre>
タグで囲み、10行を超える場合はサンドボックス(plnkr、jsbin、codepen…)を使用してください。