JavaScriptの非同期について理解する!【Promise、async / await】

JavaScriptを勉強していくと突き当たる壁として非同期処理の理解があるかと思います。わたしもぶち当たり → 分からかないから放置していると、またぶち当たり、を繰り返していくうちに覚えていきました。

非同期とは、メインの処理から外れて裏側で処理し続けてくれるような動きですね。以下のツイートが一番イメージしやすいかと思います。

避けては通れないJavaScriptの非同期について、自分が理解した観点から解説していけたらと思っております。自分が一番理解しづらかったのはasyncが返すのは、Promiseオブジェクトだというところですかね…。

非同期処理は2種類の書き方がある

まず理解しておくべきは、非同期処理の書き方は2種類あるという点ですね。

  • Promise
  • async、await

の2種類です。

冒頭でも触れましたが、asyncPromiseを返すので、土台となっているのはPromiseです。ということでまずはPromiseを理解していきましょう。

そもそも非同期がまだイメージでていませんが…

非同期は冒頭に埋め込んだツイートの内容が全てなのですが、処理的にも具体的なコードで見てみます。よく例として出されるのは以下のようなコードですね。

コピーsetTimeout(() => {
	console.log(`2、setTimeoutの処理内`);
}, 3000);
console.log("1、コードはsetTimeoutの後");

setTimeoutという関数で3秒後に実行する処理をその関数内に記載しています。コード的にはsetTimeout関すの後にconsole.log()で確認しているようなコードですね。

プログラミングは順次、選択、繰り返しが基本と言われていて、順次の原則から考えると、普通は3秒後にsetTimeout()内の処理が行われて、後続の処理が行われるような気がします。

ただ、非同期処理においてはそうはならずsetTimeout()は単独で行動を行い、処理は先に進むといった動きとなります。結果として、上記のconsole.log()の実行順序は以下のとおりです。

非同期関数ってどういうのがある?

最初に非同期の存在を知ったときに思ったが、「そういう関数は最初から教えておいてよ…」ということ。例えば、どういった関数が非同期かというと、(実はそれほどなくて)以下の2つが代表的によく見るものです。

  • setTimeout()・・・◯秒後に実行させる関数
  • json()・・・JSON形式へ変換
  • fs.readFile()・・・テキストファイルを読み取る関数(node.js)

あとは、fsのように外部から読み込む系は非同期処理になりがちな傾向があると思います。例えば、Firebase(データベース)からデータを引っ張ってくる際の処理は非同期だったりします。

Promiseを理解するための2つの結果

先ほど、「非同期の関数はどれ?」みたいな話をしましたが、非同期は自分で作ることができます。その非同期を作るためのオブジェクトがPromiseということです。

Promiseの型は以下のような感じで、成功と失敗をそれぞれ定義してあげます。

コピーconst testPromise = new Promise((resolve, reject) => {
  if (true) {
    resolve(true);
  } else {
    reject(false);
  }
});

testPromise
.then((value) => {
  console.log( "成功", value );
})
.catch((error) => {
  console.log( "失敗", error );
});

if文の中身とかは適当で恐縮ですが、完結にするためにこのような書き方としました。要するに、Promiseの中の処理において、うまくいったらresolve()、失敗したらreject()関数に返すべき値を入れてあげるという書き方が基本になります。

そして、Promiseインスタンス(testPromise)は、then()chatch()でチェーン的に繋げて処理を行います。

成功(resolve)と失敗(reject)

覚えておく必要があるのは、Promiseでは成功か失敗が定義されているということです。その書き方が独特で、成功時の結果の値をresolveの引数に、失敗時の結果の値をrejectの引数に定義しておきます。

これでPromiseの定義はOKです!
(わたしは最初のころreturnとか謎の定義をしていましたが、成功時はresolveの引数、失敗時はrejectの引数、ということを覚えて起きましょう!)

そのように作成されたPromiseインスタンスに対して、thenの方では成功したとき(resolveが呼ばれた時)の処理を、catchでは失敗した時(rejectが呼ばれた時)の処理をそれぞれ記載しておきます。

例えば、上記のコードを実行すると、成功、trueとして表示されることが分かるはずです。

結果が確定するまでthenに移らない

それにしても意味が分からなかったのですが、理解が深まったのはsetTimeoutなどの非同期をPromiseで囲んだときでした。

setTimeoutで3秒待機させても次の処理に進んでしまうという話をしましたが、Promise内に入れてしまうと結果が確定するまで(つまりresolverejectに引数が格納されるまで)thencatchに進まないようになります。

要するに、Promiseの処理においては、非同期の結果としてresolveを指定してあげれば、その非同期が終わるまでthenの処理に進まないような作りにできます。

具体的にPromiseで囲って待つための記述は以下のとおりです。3秒後にresolveを呼んであげることでPromisethenあるいはcatchへ処理は進まないようになっています。

コピーconst testPromise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("3秒経過");
  }, 3000);
});

testPromise
.then((value) => {
  console.log( "成功", value );
})

上記を実行すると、3秒後にthenが動いて、「成功 3秒経過」と表示されることが分かるはずです。

awaitはPromiseの結果を待つ

さて、Promiseの仕組みが理解できたら次はasyncとawaitです。
※ Promiseが理解できていない場合はPromiseを理解してからでないとちゃんと理解するのが難しいかもしれません…

asyncとawaitはセットで使うもので、型としてはasyncが関数の前に、awaitがPromiseの前につけます。まとめて書くと以下のような感じですね。

コピーasync function test() {
  const result = await new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve("3秒経過");
      }, 3000);
    })
  console.log( "result", result );
}
test();

awaitPromiseの処理を待って結果(resolverejectの引数の値)を受け取りconsole.logで表示されているといった動きです。

asyncでreturnしない!

わたしが最初にやっていた勘違いは、asyncの関数からawaitで受け取った値(今回だとresult)をreturnしていました。

この方が呼び出し元の関数から使えて簡潔になっていいなって思っていたからですね…。ただ、受け取ろうとしてもどうも値がおかしい…。

実は、async functionで返す値はすべてPromiseオブジェクトになるという仕組みとなっており、いくら定数を返そうがasync関数は無視してすべてPromiseとしてしまいます。

ということで、async関数でreturnしても、またその呼出もとでthenしてつなぐという冗長な処理となってしまうので、基本的にはasyncでreturnすることはないと考えておくと迷わないかも知れません。

awaitはPromiseの結果を受け取る

awaitの先に定義するのはPromiseインスタンスです。今回の例では直接記載していますが、Promise自体の定義は外に出して定義してしまったほうがスッキリするかと思います。

Promisethencatchで受け取っていたかと思いますが、awaitを使うことで直接結果を受け取ることが出きコードもかなりスッキリします。(このスッキリさが使われる理由のようです)

awaitを使うことで非同期の存在を知る前から馴染みのあった書いた順番に実行されるが、実現できるようになりました。

非同期処理はPromiseを覚えてからasync、awaitを理解する

非同期処理のベースはPromiseです。まずPromiseが理解できないことにはasyncもawaitも理解できません。

なので、「非同期処理がいまいち理解できない…」という方は、まずPromiseが正しく理解できているか自分でいろんなコードを書いて確認してみましょう。
(わたしはawaitやってはPromiseに戻りを何回も繰り返しました…)

おわり

JavaScriptの非同期について自分が苦労したところを中心に説明してみました。つまづきポイントとしては以下のとおりです。

  • Promiseの値は、成功か失敗をresolverejectの引数に入れて返す(※ returnは意味がない)
  • Promiseの結果はresolverejectが呼ばれたタイミング。呼ばれない限り、then(or catch)へは進まない
  • async関数は必ずPromiseオブジェクトを返す
  • awaitに指定するのはPromiseインスタンス。そして結果を待って返してくれる

JavaScriptの記事は文字量コード量も説明文中のコード数も多くなって、もっとスマートに書きたいですね…(簡潔に読みやすくをJavaScript勉強しながら学びました…)

関連の記事