JavaScript
promise
async
await

Promise, async, await がやっていること (Promise と async は書き換え可能か?)

恥ずかしながら、つい最近非同期の勉強を始めました。
自分なりに分かりやすいような説明で書こうと思います。
外から見たメリットなどより、中で何をやっているかを何となく理解することが今回の目的です。

1. Promise

ここでは簡単のため、resolve() だけ見ていきます。
(reject() は無いことにします)

1.1. 実行順序

イメージ
let promise = new Promise(resolve => {
    // ... Promise の中身
    // resolve(/* value */);
});

// ... 色々処理

// then そのもの
promise.then(/* value */ => {
    // ... then の中身
});

分かりやすい例で言えば、

  1. 同期的に「Promise の中身」「色々処理」「then そのもの」が実行される
  2. 「Promise の中身」およびそこで設定したイベントで resolve() が呼ばれる
  3. 「then の中身」が呼ばれる

と実行されます。
ただし実際には、イベントの発生タイミングや、「Promise の中身」で直接 resolve() した場合などで、1, 2, 3 の実行順序が入れ替わります。

  • resolve() 時に then されていないとき
    • resolve() した値をとっておき、then 時に「then の中身」を実行
  • then 時に resolve() されていないとき
    • resolve() 時に「then の中身」を実行

つまり then と resolve() の両方が呼ばれて初めて「then の中身」が実行されます。

1.2. Promise チェーン

1.2.1. then がやっていること

then がやっていることは他にもあります。
「then の中身」が値を return したとき、

  • その値が Promise ならば、そのまま返す (ように見える)
  • その値が Promise でなければ、値を resolve() する Promise を返す (ように見える)

(他に thenable なるものがありますが、ここでは省略)

Promise でない値を返す
let promise = new Promise(resolve => {
    setTimeout(resolve, 1000, 'foo');
});

promise.then(value => {
    console.log({value}); // {value: 'foo'}
    return 'bar'; // ★
}).then(value => {
    console.log({value}); // {value: 'bar'}
});
Promise を返す
let promise = new Promise(resolve => {
    setTimeout(resolve, 1000, 'foo');
});

promise.then(value => {
    console.log({value}); // {value: 'foo'}
    return new Promise(resolve => { // ★
        setTimeout(resolve, 1000, 'bar');
    });
}).then(value => {
    console.log({value}); // {value: 'bar'}
});

この 2 つの例がやっていることは別のことですが、同じことをやるときは以下のようになるでしょう。

    return 'bar';

    // ↓

    return new Promise(resolve => {
        resolve('bar');
    });

1.2.2. then はコールバック関数の return 値の Promise をそのまま返しているのか?

答えは NO です。
JavaScript から見たら、「then そのもの」が実行された段階では「then の中身」が何を返すか分からないので、実際はもっと複雑です。
実際の実装に関しては以下が詳しいです。

参考「Promiseの実装をしっかり読んでみたので学習メモ - Qiita

2. async/await

2.1. async 関数

function や アロー関数に async を付けると「async 関数」を定義できます。
「async 関数」とは以下のようなことができます。

  • (見かけ上) 関数そのものが非同期に実行される
  • (実際は) Promise を返す
    • returnresolve() 扱い
    • 例外等の throwreject() 扱い

ただし、非同期 (に見えるよう) に使うためには、async 関数内で await (後述) を使う必要があり、await を含まない「async 関数」は意味がない (普通の関数で良い) ものになります。

イメージ
let asyncFunction = async () => {
    // ... 中身
    // await /* Promise */;
};

// 本当は Promise を返しているが、実行するだけで
// 戻り値が不要な場合は普通の関数のように記述できる
asyncFunction();

2.2. await

Promise に await を付けると、Promise が resolve() するのを待ってその値を取得する (ように見えます) 。

戻り値が不要な場合 ※
    let promise = new Promise(resolve => {
        setTimeout(resolve, 1000); // 中で resolve() が呼ばれる
    });

    await promise; // 1 秒待機

    console.log('foo');
戻り値を参照 ※
    let promise = new Promise(resolve => {
        setTimeout(resolve, 1000, 'foo');; // 中で resolve('foo') が呼ばれる
    });

    let value = await promise; // 1 秒待機

    console.log(value);

※ただし、await は async 関数内でないと使えないという制約があるので、実際には上記のコードを async 関数内に書く必要があります。
async 関数外で await を使いたい場合は、async 即時関数 を使うのが手っ取り早いです。

async 即時関数
(async () => {
    // ... (await を使ったコード)
})();

2.3. async/await は 1 対 1 対応なのか?

async 関数の中には await がないと意味がなく、await を使いたいときは async 関数内でないといけませんが、await の後ろには async 関数を実行したものだけでなく Promise も置けるので、「1 対 1」と言い切ってしまうと誤解を生む言い方な気がします。

Promise を直接 ※
    let promise = new Promise(resolve => {
        setTimeout(resolve, 1000, 'foo');
    });

    let value = await promise; // ★

    console.log(value);
Promise を作る関数 ※
    let newPromise = value => new Promise(resolve => {
        setTimeout(resolve, 1000, value);
    });

    let value = await newPromise('foo'); // ★

    console.log(value);
async 関数 ※
    let sleep = delay => new Promise(resolve => {
        setTimeout(resolve, delay);
    });

    let asyncFunction = async value => {
        await sleep(1000); // 
        return value;
    };

    let value = await asyncFunction('foo'); // ★

    console.log(value);

※同じく async 関数内

3. Promise と async は書き換え可能か?

答えは NO と言っていいと思います。

(可能な場合もあります)

3.1. async → Promise

async を Promise に書き換えることはできますが、メリットはあまりないように思います。

書き換え前 ※
    let sleep = delay => new Promise(resolve => {
        setTimeout(resolve, delay);
    });

    let asyncFunction = async value => {
        console.log('sleep start');
        await sleep(1000);
        console.log('sleep end');
        return value;
    };

    let value = await asyncFunction('foo');

    console.log(value);
書き換え後
let sleep = delay => new Promise(resolve => {
    setTimeout(resolve, delay);
});

let asyncFunction = value => new Promise(resolve => {
    console.log('sleep start');
    sleep(1000).then(() => {
        console.log('sleep end');
        resolve(value);
    });
});

asyncFunction('foo').then(value => {
    console.log(value);
});

※同じく async 関数内

3.2. Promise → async

特定の場合にしか書き換えることができません。

特に、setTimeout() のように Promise 以外のイベントハンドラに resolve() を設定するような処理をする Promise は async に書き換えることは不可能です。
(async 関数は常に最後で resolve() するため)