Open33

JSのPromiseとasync/awaitを学ぶ

Yug (やぐ)Yug (やぐ)

同期ってなんだっけ?非同期ってなんだっけ?
Promiseって何?
async/awaitは挙動はわかるけど仕組みを言語化できないぞ、なんなんだこれ?

になったので基礎を固める

Yug (やぐ)Yug (やぐ)

https://zenn.dev/ak/articles/dc23436b241a84
https://www.youtube.com/watch?v=wl_oOMFa1MM&t=1s

んん...?「重い処理は裏側で実行しておく、それが非同期処理だ」って、それ並行処理とか並列処理とかそういう話では?

そもそも「裏側で実行する」という最強技が使えるならそれは同期非同期の話じゃなくてただ最強・理想っていう感じがする

「重い処理はあとでやるために一旦飛ばすね」ってだけのことをやれるのが非同期の概念じゃないの?

違和感

Yug (やぐ)Yug (やぐ)

ようわからん

PromiseはES2015で導入された非同期処理の状態や結果を表現するビルトインオブジェクトです。 非同期処理はPromiseのインスタンスを返し、そのPromiseインスタンスには状態変化をした際に呼び出されるコールバック関数を登録できます。

Yug (やぐ)Yug (やぐ)

ふむふむ

Promiseも3つの状態を持ちます。
まず最初は「pending」という保留中の状態になって、成功したら、「fulfilled」と呼ばれる状態になります。そして、もし失敗や例外が起こったら「rejected」と呼ばれる状態になります。

  1. pending
  2. fulfilled
  3. rejected
Yug (やぐ)Yug (やぐ)

ピザの例えわかりやすい

ピザを注文すると、アプリの中で「準備中」か「配達済み」か「キャンセルされたか」の3つの状態を教えてくれます。

このように、Promiseの仕組みを使えば、注文したピザの状態を追跡するように、非同期処理の状態や結果を追跡できます。

Yug (やぐ)Yug (やぐ)

ほんとだ、3種類できた。楽しい

const a = new Promise((resolve, reject) => reject('失敗したよ'));
console.log(a);

const b = new Promise((resolve, reject) => resolve('成功したよ'));
console.log(b);

const c = new Promise((resolve, reject) => {});
console.log(c);

Yug (やぐ)Yug (やぐ)

内部でconsole.log書いちゃえば外部でconsole.log書かなくてもログ出てた。へぇ

const a = new Promise((resolve, reject) => resolve('成功だよ')).then((text) => 
  console.log(text + 'ないす')
);

const b = new Promise((resolve, reject) => reject('失敗だよ')).catch((text) => 
  console.log(text + 'だめだ')
);

んでPromiseの閉じ括弧はresolveもしくはrejectのとこですぐ閉じちゃってokなのね
then/catchは完全に別のスコープ的になるのか、なるほど

Yug (やぐ)Yug (やぐ)

thenとcatchは繋げて書ける。確かにresolveとrejectを変えてみたらthen/catchがちゃんと切り替わってた

const a = new Promise((resolve, reject) => resolve('成功だよ'))
  .then((text) => console.log(text + 'おはよう'))
  .catch((text) => console.log(text + 'おやすみ'));
Yug (やぐ)Yug (やぐ)

あーfinallyもあるのね

また、Promiseオブジェクトは then catch finallyの3つのメソッドをもち、いずれもまたPromiseオブジェクトを返します。

めっちゃ思った

なんとなく try - catch - finallyの構文に似ていますね。

Yug (やぐ)Yug (やぐ)

ふむふむ(finallyは引数を取らない)

thenのコールバック関数の引数にPromiseの結果、catchのコールバック関数の引数にはPromiseが失敗した理由(Errorが入ることが多い)が渡されます。

// const promise = Promiseオブジェクト; これ以降のコードでは省略

promise.then(result => { 
  // resultにはPromiseの返り値が代入される
}).catch(reason => {
  // reasonにはPromiseが失敗した理由(Errorほか)が代入される
}).finally(() => {
  // 引数をとらない。fulfilledでもrejectedでも呼び出される
});

引数はresultとreasonという名前にした方が良さそうだな

Yug (やぐ)Yug (やぐ)

なるほど

Promiseオブジェクトの作り方は主に、

  1. fetchなどの関数の返り値
  2. async functionキーワードを用いた非同期関数
  3. Promiseコンストラクタ new Promise((resolve, reject) => { ... })

があります。基本的には1つ目のように使うことが多く、直接的にPromiseオブジェクトを生成することは少ないと思います。

Yug (やぐ)Yug (やぐ)

あ、エラー投げるのでもrejectedになるのか

rejectに値を渡す、もしくはErrorをthrowすることでPromiseはrejectedになる

ほんとだ、以下試してみたら、

const a = new Promise((resolve, reject) => {
  throw new Error('だめだ');
})
  .catch((text) => console.log(text + 'おやすみ'));

ちゃんとrejectedされてるしエラーオブジェクトがcatchの引数に入ってる

Yug (やぐ)Yug (やぐ)

へぇぇぇ

thenメソッドはもうひとつコールバック関数をとることができます。この2つ目のコールバック関数はPromiseが失敗したときに呼び出されます。つまり、catchの内容をthenに書くことができます。実際に、catchメソッドは内部的にthenメソッドを呼び出すとされています。

promise.then(result => { ... }).catch(reason => { ... });
// 上下でやっていることは同じ
promise.then(result => { ... }, reason => { ... });

おぉ、ほんとだすげぇ

const a = new Promise((resolve, reject) => reject('失敗だよ'))
  .then(
    result => console.log('おはよう', result),
    reason => console.log('おやすみ', reason)
  );

Yug (やぐ)Yug (やぐ)

Promiseの無限チェーンもできた。なるほどな、前のthenでreturnされたものが次のthenの引数になる感じか

const a = new Promise((resolve, reject) => resolve(1))
    .then(result => result + 1)
    .then(result => result + 1)
    .then(result => console.log(result));  // 3
Yug (やぐ)Yug (やぐ)

へぇぇ

async関数はPromiseオブジェクトを返す

ほんまや!へぇ

async function a() {
  return "Hello!";
}
console.log(a());

Yug (やぐ)Yug (やぐ)

なのでPromiseということは当然thenで受け取れば普通に出力できる

async function a() {
  return "Hello!";
}
const result = a().then(result => console.log(result));
console.log(result);

Yug (やぐ)Yug (やぐ)

ふむ、その「行で止まって待つ」という感じ

await演算子は直後のPromiseオブジェクトがresolveされるのを待って結果を返す

async function asyncHello() {
  // 3秒待つPromise = 擬似的なwaitはこう書けます
  await new Promise(resolve => {
    window.setTimeout(() => {
      resolve();
    }, 3000);
  });
  // 3秒経ったらHelloを返します
  return "Hello!";
}

console.log(asyncHello());
// 3秒後に"Hello!"と表示したい

なるほどそういうことか

awaitは「直後のプロミスオブジェクト(がresolveされるの)を待つ演算子」ていう言語化ができるようになったのはでかい

Yug (やぐ)Yug (やぐ)

んでawaitの行でrejectされたらエラーがthrowされる

なお、await演算子の直後のPromiseオブジェクトがrejectされた場合は、エラーがthrowされます。

つまりただのconsole.errorとかではなくthrow new Errorと同じということだと思うので、ランタイムエラーみたいな感じでそのawaitの行で処理が中断する

Yug (やぐ)Yug (やぐ)

これはawaitつけてもつけなくても結果変わらなかった。awaitの後にresolve/rejectは起きないので

async function a() {
  await setTimeout(() => {
    console.log('fire');
  }, 3000);
  console.log('aaa');
}

a();
Yug (やぐ)Yug (やぐ)

たしかにそういわれればfetchはPromiseを返しているのか。いつもawaitしてるってことは

Promiseを返す関数といえばブラウザには fetchがあります。

Yug (やぐ)Yug (やぐ)

へぇぇ

fetch関数はHTTPステータスのエラー(404, 500など)があってもrejectはしません。rejectされるのは、CORS関連のエラーや、ネットワーク接続のエラーなどが発生したときに限られます。

ほんとだ、httpステータスエラーだけなら問答無用でresolveされてる...!

fetch("https://httpstat.us/418")
  .then(result => result.text()) // 400番台だからrejectと思いきやfulfilled。
  .then(text => console.log(text))
  .catch(reason => console.error(reason)); // ネットワークエラーをcatch
// コンソールに 418 I'm a teapot が出てくる (RFC2324)

CORSエラーやネットワーク接続エラーのみrejectされるのか

Yug (やぐ)Yug (やぐ)

ふむふむ

直列処理においてコールバックのネストが深くなることを防ぐための手段としても、Promiseは有効なのです。

Yug (やぐ)Yug (やぐ)

なるほど、urlを配列に格納しておいてその中をforEachで回ってfetchすると綺麗に書けるな

const targets = [
  "https://httpstat.us/200",
  "https://httpstat.us/201",
  "https://httpstat.us/202",
  "https://httpstat.us/203"
];
targets.forEach(target => {
  fetch(target).then(result => result.text()).then(text => console.log(text));
});
Yug (やぐ)Yug (やぐ)

あー確かに。だからこそawaitを使えば対処できるのか。awaitは非同期を同期的に処理するみたいなやつってことよね

前節では、リクエストはしっかり並列して行われています。しかし、返り値(ログの出力)の順番が一定ではありません。

Yug (やぐ)Yug (やぐ)

えぇぇ

forEachのコールバック関数をasyncにするパターンです。が、この処理はうまく実行されません。
forEachのコールバック関数の中ではawaitされますが、コールバック関数の実行そのものはawaitされないためです。

うわ、ほんとだ

const targets = [
  "https://httpstat.us/200",
  "https://httpstat.us/201",
  "https://httpstat.us/202",
  "https://httpstat.us/203"
];
targets.forEach(async target => {
  console.log('targetは', target);
  await fetch(target).then(result => result.text()).then(text => console.log(text));
});

うーんこれむずかしいなぁ、なんでこうなるんだろう。「関数自体の実行はawaitされない」とは?

targetはちゃんと順番通りなのになぜだ

Yug (やぐ)Yug (やぐ)

まぁとりあえずの仮説

「forEachのような高階関数のコールバック関数として(async)関数を作った場合、その関数は別のメモリ領域に保存されてしまい、実行がバラバラになり得る」

Yug (やぐ)Yug (やぐ)

へーなるほどなぁ

Promise.allもまたPromiseを返します。Promise.allは、引数として渡された配列内のすべてのPromiseがfulfilledになる(または1つでもrejectされる)のを待って、その結果をresolve / rejectします。

返すのはPromise、そしてthenの引数に入ってくるのはすでにresolveされたもの(=Promiseではない)、という点はPromiseと同じ

ほう、逆にPromiseの実行順序は保証されていないのか

ちなみに、Promise.allの中のPromiseの実行順序は保証されていません(並列実行される)が、返り値の配列は呼び出し順と必ず同一になるようになっています。

Yug (やぐ)Yug (やぐ)

Promise.allの引数は、

要素がPromiseの配列!

つまりPromiseを返す処理をmap関数内に書いている場合などが当てはまる

Yug (やぐ)Yug (やぐ)

Promise.allSettled

Promise.allとの違いは、たとえ1つPromiseがrejectされても処理が続くことです。

Yug (やぐ)Yug (やぐ)

Promise.any

引数のPromiseの実行が1つでも正常に完了するもしくはすべて失敗するとresolve/rejectされます。
1つのPromiseがresolveされると、あとのPromiseの結果はすべて無視されます。

なるほど

Promise.allと逆の性質を持っているとも言えますね。

なるほど

複数のAPIエンドポイントにリクエストし、一番最初にレスポンスしたものを使いたい、そんなときに便利な新メソッドです。

Yug (やぐ)Yug (やぐ)

Promise.race

引数のPromiseの実行が1つでも正常に完了するもしくは1つでも失敗するとresolve/rejectされます。