JavaScriptを勉強していくと突き当たる壁として非同期処理の理解があるかと思います。わたしもぶち当たり → 分からかないから放置していると、またぶち当たり、を繰り返していくうちに覚えていきました。
非同期とは、メインの処理から外れて裏側で処理し続けてくれるような動きですね。以下のツイートが一番イメージしやすいかと思います。
Q. 非同期処理ってなんですか?
A.
コンビニの店員さんが、「温めますか?」と言った後、温めてる間に別のお客さんの会計やりつつ、温め終わったら商品くれるやつ— いちくん@25歳独学未経験でWeb制作エンジニアになったひと (@ichikun0000) July 16, 2020
避けては通れないJavaScriptの非同期について、自分が理解した観点から解説していけたらと思っております。自分が一番理解しづらかったのはasync
が返すのは、Promiseオブジェクトだというところですかね…。
非同期処理は2種類の書き方がある
まず理解しておくべきは、非同期処理の書き方は2種類あるという点ですね。
Promise
async、await
の2種類です。
冒頭でも触れましたが、async
はPromise
を返すので、土台となっているのは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
内に入れてしまうと結果が確定するまで(つまりresolve
かreject
に引数が格納されるまで)then
かcatch
に進まないようになります。
要するに、Promise
の処理においては、非同期の結果としてresolveを指定してあげれば、その非同期が終わるまでthen
の処理に進まないような作りにできます。
具体的にPromise
で囲って待つための記述は以下のとおりです。3秒後にresolve
を呼んであげることでPromise
はthen
あるいは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();
await
がPromise
の処理を待って結果(resolve
かreject
の引数の値)を受け取りconsole.log
で表示されているといった動きです。
asyncでreturnしない!
わたしが最初にやっていた勘違いは、asyncの関数からawaitで受け取った値(今回だとresult
)をreturn
していました。
この方が呼び出し元の関数から使えて簡潔になっていいなって思っていたからですね…。ただ、受け取ろうとしてもどうも値がおかしい…。
実は、async function
で返す値はすべてPromise
オブジェクトになるという仕組みとなっており、いくら定数を返そうがasync関数は無視してすべてPromise
としてしまいます。
ということで、async関数でreturnしても、またその呼出もとでthenしてつなぐという冗長な処理となってしまうので、基本的にはasyncでreturnすることはないと考えておくと迷わないかも知れません。
awaitはPromiseの結果を受け取る
await
の先に定義するのはPromise
インスタンスです。今回の例では直接記載していますが、Promise
自体の定義は外に出して定義してしまったほうがスッキリするかと思います。
Promise
をthen
かcatch
で受け取っていたかと思いますが、await
を使うことで直接結果を受け取ることが出きコードもかなりスッキリします。(このスッキリさが使われる理由のようです)
await
を使うことで非同期の存在を知る前から馴染みのあった書いた順番に実行されるが、実現できるようになりました。
非同期処理はPromiseを覚えてからasync、awaitを理解する
非同期処理のベースはPromise
です。まずPromiseが理解できないことにはasyncもawaitも理解できません。
なので、「非同期処理がいまいち理解できない…」という方は、まずPromiseが正しく理解できているか自分でいろんなコードを書いて確認してみましょう。
(わたしはawait
やってはPromise
に戻りを何回も繰り返しました…)
おわり
JavaScriptの非同期について自分が苦労したところを中心に説明してみました。つまづきポイントとしては以下のとおりです。
- Promiseの値は、成功か失敗を
resolve
かreject
の引数に入れて返す(※return
は意味がない) - Promiseの結果は
resolve
かreject
が呼ばれたタイミング。呼ばれない限り、then
(orcatch
)へは進まない - async関数は
必ず
Promiseオブジェクトを返す - awaitに指定するのはPromiseインスタンス。そして結果を待って返してくれる
JavaScriptの記事は文字量コード量も説明文中のコード数も多くなって、もっとスマートに書きたいですね…(簡潔に読みやすくをJavaScript勉強しながら学びました…)