※スマホ対応はしてません。

タグ: 非同期

非同期やるならasync/awaitでもっとらくらく。(現代的JavaScriptおれおれアドベントカレンダー2017 – 11日目)

カテゴリー: JavaScript

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

概要

Promise を使った非同期の処理をさくさく書けるやつです。

簡単な例としては、まず Promise オブジェクトを返す関数↓があったとして、

// 指定ms待つ
function sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}

このコード↓を

function sayAfterSleeping(message) {
    console.log('ぐー……');
    sleep(1000)
        .then(() => {
            console.log('むにゃ……');
            return sleep(1000);
        })
        .then(() => {
            console.log('あ、ごめん寝てた。えっと……');
            return sleep(1000);
        })
        .then(() => {
            console.log(message);
        })
}

sayAfterSleeping('Hello Async World!');

こう↓書けます。

async function sayAfterSleeping(message) {
    console.log('ぐー……');
    await sleep(1000);

    console.log('むにゃ……');
    await sleep(1000);

    console.log('あ、ごめん寝てた。えっと……');
    await sleep(1000);

    console.log(message);
}

sayAfterSleeping('Hello Async World!');

基本的な使い方

Promise を使ったコードを、前述のようにそれなりに簡単に置き換えることができます。

  1. Promise を返す関数の呼び出しに await を付ける
  2. await を使う関数に async を付ける

これだけです。

忘れないでね

async を付けるのは Promise オブジェクトを返す関数ではなく、 await を使う方の関数という点にご注意ください。逆に Promise オブジェクトを返すやつに async を付ける必要は(その中で await を使っていないなら)ないです。

あ、あと await を書けるのは async を付けた関数の中だけです。

ここら辺最初のころは忘れたり混同したりしがちでは。(個人の感想です。)

結果を受け取る

then() で登録するコールバック関数実行時に結果を受け取ることができましたが、 await の場合は単純な戻り値のような形で扱えます。

// fetch()とformat()はダミーです

function fetchDetail(id) {
    return fetch(`/api/foo/${id}`)  // Promiseを返す
        .then((res) => {
            return format(res);  // 結果を整形
        });
}

async function showDetail(id) {
    const detail = await fetchDetail(id);  // 待ってから結果を受け取る
    console.log(detail.name, detail.type);
}

showDetail(123);

Fetch APIの話じゃないです。

for 文

実践編。

間を置きながら n 回数えて終了する countdown() という処理を考えます。

真ん中が本体です。

// 指定ms待つ
function sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}

function countdown(n) {
    let p = Promise.resolve();
    for (let i = 0; i < n; i++) {
        p = p.then(() => {
            console.log(`${n - i}...`);
            return sleep(1000);
        });
    }
    return p;
}

countdown(3)
    .then(() => {
        console.log('done!')
    })

なんかややこしいすね。

単純な for 文だと駄目です。だって Promise 完了まで待たないとすぐ終わっちゃうから。なので、 Promise オブジェクトを変数 p で覚えておいて、さらにそこから then() の戻り値で上書きしながら連結します。(ちなみに let を var にするとだめです。)

これ↑を、 await を使うことで、こう↓めっちゃ簡単に書けるようになります。

// 指定ms待つ
function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

async function countdown(n) {
  for (let i = 0; i < n; i++) {
    console.log(n - i, '...');
    await sleep(1000);
  }
}

countdown(3)
  .then(() => {
    console.log('done!')
  })

ほら、さっき駄目だった「ただの for 文」で済んでしまいました。読みやすーい。

宣言、式、メソッド

各種取り揃えております。

async function foo() {}

const bar = async function() {};

const obj = {
  async hoge() {},
};

class Klass {
  async fuga() {}
}

例外

Promise オブジェクトの catch() は、普通の try-catch で実現できます。

async function() {
  try {
    const result = await doAsync();
  }
  catch (err) {
    console.error(err);
  }
}

いろいろな await

途中に await

わりとどこにでも await を突っ込むことができるようです。

function returnLater(result) {
  return new Promise(resolve => setTimeout(() => resolve(result), 1000));
}

async function af () {
  console.log('...');
  console.log(await returnLater(1) + await returnLater(2));  // 3
}

af();

ただ await 出現の時点で待機を開始するので、複数書いても並列に実行はしません。上記の例では1000 ms待つのを二回呼んでるので、結果が出てくるまで2000 msかかります。

await new Promise()

というのもできます。要は右辺が Promise オブジェクトなら良いわけで、関数実行は結果としてそれを満たすだけです。

async function foo() {
  await new Promise(resolve => setTimeout(resolve, 1000));
  console.log('おまたせ!');
}

await 123

というのもできます。右辺が Promise オブジェクトだったら待つんだけど、そうでなければそのまま流します。

await await await p

というようにたくさん並べることもできます。

できるけどしないね。(関数呼び出しの中身を追っていくと結果的にこうなる場面はいくらでもあるだろうけど。)

ES2017

ES2015 (ES6)じゃなくてES2017で追加されました。

ChromeとFirefoxではもう使えます。それ以外で使うならバベってください。

その他

async 後に改行は駄目

だめです。

async
function foo() {}
// Exception: ReferenceError: async is not defined

まあこんなのしないよね。

Function オブジェクトではない

async 付きで宣言したものは、コンストラクタが Function ではなく AsyncFunction になります。

console.log((async function(){}).constructor);  // => function AsyncFunction()

とはいえ AsyncFunction が Function を継承しているので、 instanceof は普通に動きます。ご安心ください。

console.log((async function(){}) instanceof Function);  // => true

AsyncFunction コンストラクタ

前項の通り、 AsyncFunction コンストラクタは存在するんだけど、グローバルオブジェクトにはなってません。

const af = new AsyncFunction();  // Exception: ReferenceError: AsyncFunction is not defined

どうしても使いたければ、インスタンスから探っていきます。

const AsyncFunction = (async function(){}).constructor;
const af = new AsyncFunction();

使い方は Function といっしょ。

“async” の読み方

「アシンク」より「エイシンク」くらいっぽいです。まあ日本語で会話してるところにエィスィ↑ンクとか言っても逆に通じないだろうけど。

英単語 “async”, “await” の意味

“async[hronous]” は「非同期」。皆様ご存知ですね。

“await” は「待ち受ける」だそうです。

参考

非同期やるならPromiseでらくらく。(現代的JavaScriptおれおれアドベントカレンダー2017 – 10日目)

カテゴリー: JavaScript

現代的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 の静的メソッド各種に言及(ちょっとだけ)