現代的JavaScriptおれおれアドベントカレンダー2017 – 10日目

概要

メソッドチェインみたいにして、処理を順々に書いていけます。

// Promiseオブジェクトを返す関数
function doLater() {
  return new Promise((resolve, reject) => {
    const result = 123
    setTimeout(() => resolve(result), 1000);
  });
}

doLater()
  .then(result => {
    console.log('(1/2)', result);

    return doLater2();  // Promiseオブジェクトを返す関数
  })
  .then(result => {
    console.log('(2/2)', result);
  })
  .catch(err => {
    console.error(err);
  });

わかっちゃえば簡単だと思うんだけど、慣れないと戸惑うかも。

旧世代: コールバック

非同期処理やるとなると、コールバックを与えて終わったら呼び返してもらう、というのがかつては普通でした。

console.log('Ready.');

doLater(() => {
  console.log('Done.');
});

console.log('Working...');

doLater() が非同期に実行してコールバックするとして、出てくるのは “Ready.” → “Working…” → “Done.” の順になります。

Welcome to the Callback Hell!

まあちょっと使うくらいなら単純で何も問題ないんだけど、これが複数になると、やばい。

console.log('Start!');
doLater1(() => {
  doLater2(() => {
    doLater3(() => {
      doLater4(() => {
        doLater5(() => {
          console.log('Done!');
        });
      });
    });
  });
});

なかなかインデント・ハドウケンって感じで大変な趣き深さがあります。

こんなインデントが厳しくなるようなコールバックに次ぐコールバックを、コールバック地獄(rtヘル)と呼びます。

そしてプロミスへ

Promiseというのを使うと、インデントが深くならずに済みます。

console.log('Start!');
doLater1()
  .then(() => doLater2())
  .then(() => doLater3())
  .then(() => doLater4())
  .then(() => doLater5())
  .then(() => console.log('Done!'));

基本的な使い方

こんな感じ。実行順序にご注意ください。

// 1. `new Promise()` して新しい `Promise` オブジェクトを作成
const p = new Promise((resolve, reject) => {
  // 3. 非同期処理実行
  setTimeout(() => {
    // 4. 完了時に `resolve()` を実行
    const result = 123;
    resolve(result);
  }, 1000);
});

// 2. `Promise` オブジェクトに `then()` でコールバックを登録
p.then((result) => {
  // 5. `resolve()` に与えた情報を伴ってコールバック実行
  console.log(result)
});

前半、1と3、4のところが doLater() になる感じですね。

非同期処理といってもスレッドが分かれているわけではないので、 new Promise() の中で無限ループとかしたら、ブラウザは固まります。

戻り値を返す

まあ戻り値じゃないんだけど、処理をした「結果」として、 resolve() で何かひとつの情報を return することができます。その値は then() で実行されるコールバック関数へ引数として渡されます。

Promise オブジェクト

使い方はまあそこら中に教えてくれるひとがいると思うので、ちょっと仕様的な話の方を。

内部に持っている情報

new Promise() して戻ってくる値は、内部に以下の情報を持っています。

  • 状態。以下のいずれか:
    • pending … 待機
    • fulfilled … 満足(成功)
    • rejected … 拒否(失敗)
  • 結果
  • 満足(成功)時に実行するコールバックのリスト
  • 拒否(失敗)時に実行するコールバックのリスト

executer

new Promise() に与える関数を “executer” と呼ぶみたいです。

このexecuterには二つの引数、関数オブジェクトの resolve と reject が与えられます。

内部で持っている状態はもちろん “pending” から始まり、この resolve() ないし reject() を実行することで変化します。

ちなみに一度変化した後に resolve() 、 rejest() を呼んでも何も起こりません。

コールバック

then() や catch() を使って、コールバックリストへ関数を追加しておくと、状態が変化した際にいずれかのリストが実行されます。

then() で追加しようとした際に既に満足なり拒否なりの状態になっている場合は、その追加しようとしていた関数はすぐ実行されます。

then()

p.then().then()... と繋げることができますが、実は then() 実行のたびに新しい Promise オブジェクトが生成されています。

const p0 = new Promise(resolve => resolve())
const p1 = p0.then(() => undefined)
const p2 = p0.then(() => undefined)
const p3 = p1.then(() => undefined)

console.log(p1 === p0);  // false
console.log(p1 === p2);  // false
console.log(p3 === p1);  // false

複雑なコールバック連携

then() で何か値を return することで、結果として渡す情報を変更することができます。

さらにここで Promise オブジェクトを返すと、その完了(満足なり拒否なり)まで待ってから次へ進むことになります。

// 指定ms待ってから、指定の情報を伴ってコールバック
function sleep(ms, result) {
  console.log(`waiting for ${ms} ms`);
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(result);
    }, ms);
  });
}

// 1000 ms待って、100を返す
sleep(1000, 100)
  .then((result) => {
    // resolve()に与えられた情報を受け取る
    console.log(result);  // 100
  
    // 何もreturnしなければ、
  })
  .then((result) => {
    console.log(result);  // undefined

    return 123;  // 何か値を返すと、
  })
  .then((result) => {
    // 次の仮引数で新しい値の方を受け取れる
    console.log(result);  // 123

    // 新しいPromiseオブジェクトを返すと
    return sleep(1000, 234);
  })
  .then((result) => {
    // Promiseが解決されるまで待ってから、
    // 新しいPromiseの実行結果を得られる
    console.log(result);  // 234
  })

catch()

拒否(失敗)は catch() で拾います。 then() チェインのどこかで失敗した場合は catch() まで飛びます。

Promise.resolve()
  .then(() => {
    console.log(1);
    throw new Error('#1');
  })
  .then(() => {
    console.log(2);
  })
  .catch((error) => {
    console.error(error);
  })
  .then(() => {
    console.log(3);
  });

1 → Error → 3の順にコンソールに出力されます。 2 は、その前にエラーになったので実行されません。

try-catch みたいな感じ。

チェインさせなければ実行される

チェインという表現で良いのかわかんないですけど、前述の通り then() のたびに新しい Promise オブジェクトが生成されます。もし最初のオブジェクトに then() や catch() をぶら下げた場合は、その最初の Promise オブジェクトの結果だけが影響します。

const p0 = Promise.resolve()
p0.then(() => {
  console.log(1);
  throw new Error('#1');
});
p0.then(() => {
  console.log(2);
})
p0.catch((error) => {
  console.error(error);
})
p0.then(() => {
  console.log(3);
});

これなら 1 も 2 も 3 出力され、逆に catch() は 1 のところの例外を拾いません。

成功失敗に依らないコールバック

jQueryの deferred.always() 的なもの、 try-catch の finally 的なものは、ES2017までに存在しません。

代わりに catch() で枝分かれを収束させた後に then() すると、両方の場合に対応できます。

// くるくる表示
showLoading();

// サーバへ情報を送る
data.save()
  .catch(() => {
    // 失敗時の処理
  });
  .then(() => {
    // 成功しても失敗してもくるくる非表示
    hideLoading();
  })

一度catchするのがポイント。

finally()

まだないんだけど、proposalに出てるようです。ES2018で追加されるかも?

Chrome、Firefoxそれぞれ次のバージョンで入るっぽい。

// くるくる表示
showLoading();

// サーバへ情報を送る
data.save()
  .finally(() => {
    // 成功しても失敗しても、くるくる非表示
    hideLoading();
  });

繰り返しますけど、まだないです。

その他

then() の引数

実は第二引数に拒否(失敗)時のコールバックを指定できます。

p.catch(f) は p.then(undefined, f) と等価です。内部でそう実行してます。

resolvedな状態

仕様的には、 reject() すると”rejected” 状態になるのに、 resolve() すると “resolved” じゃなくて “fulfilled” 状態という呼び方になってます。なんでや。

ちなみに “full-” じゃなくて “ful-” 。

fulfilled/rejected 状態の Promise を一発で作る

Promise.resolve() 、 Promise.reject() というメソッドがあり、待機終了した状態のインスタンスを一発で作れます。試験時とかループしながら連結するときとか、ときどき便利。

複数の Promise オブジェクトを制御する

全て完了するまで待つ Promise.all() と、 ひとつでも完了したら進む Promise.race() というのもあります。

参考

更新履歴

  • 2017-12-10 満足(成功)、拒否(失敗)に依らない処理を、併記から catch().then() へ修正(ご指摘頂きました。ありがてえありがてえ)
  • 2017-12-10 「jQueryの “completed” 的なもの」という表現を「jQueryの deferred.alway() 」へ変更
  • 2017-12-10 「 catch() 」を追加
  • 2017-12-10 「チェインさせなければ実行される」を追加
  • 2017-12-10 Promise の静的メソッド各種に言及(ちょっとだけ)