Javascript初心者であれば、必ずつまずく「非同期処理」についてまとめました。
文中のコードはコピペするだけで実際に以下のサイトでも動くことを確認していますので、
ぜひ、実際にコードをコピペしてデバッグしていただきながら読んでいただけると理解が深まると思います。
非同期処理と同期処理の違い
同期処理・・・上から順番に実行される処理になります。
console.log("A:ボタンクリック")
console.log("B:APIを叩いてデータを取得して、加工");
console.log("C:新しい画面の表示");
A → B → C
の順番に処理が実行されたと思います。
それではもっと複雑な処理を考えてみましょう。
もし、Bの処理が非常に重い処理の場合どうなってしまうでしょうか?
仮のコードで体験してみましょう。
console.log("A:ボタンクリック");
// ここでは「重い処理」を体験するために強制的に処理が重くなるような処理を書いています。
const fetchApi = () => {
console.log("B:APIを叩いてデータを取得して、加工");
const startTime = new Date();
while ( new Date() - startTime < 5) {
console.log("Bデータ取得中");
}
}
// 関数の実行
fetchApi();
console.log("C:新しい画面の表示");
このコードを実行するとわかると思いますが、A → B → C
の順番に問題なく処理は実行されます。しかし「データ取得中」に結構な時間を使っていることがわかると思います。
では、この「データ取得中」の正体はなんでしょうか?→→→「画面フリーズ」
です。
画面がロックされて全く操作できない状況のことです。皆さんも経験あると思います。
このような動きでも問題ありませんが、ユーザーにとって「画面がフリーズする」ということはストレスでしかありません。
問題を改めて整理してみましょう。
この実装では何が問題なのでしょうか?(何が原因でユーザー体験が悪くなってしまっているのでしょうか?)
原因は、新しい画面が表示されることはなく、フリーズをしてしまっている(画面操作が全くできない)ですよね。
根本的な解決は、サーバーサイド側でAPIのレスポンスを高速にすることですが何事にも限界があります。Javascript側でこの「画面フリーズ」問題を解決するにはどうしたらいいでしょうか?
解決策として以下が考えられます。
- Aを実行
- Cを一旦実行してユーザーが自由に画面を操作できるようにする
- Cが完了したあとBの処理(重い処理)を実行する
フリーズしてしまうことがユーザー体験悪化の原因なので、このような処理を行うことでフリーズを防ぐことができます。
では、実装してみましょう。
console.log("A:ボタンクリック");
const fetchApi = () => {
console.log("B:APIを叩いてデータを取得して、加工");
const startTime = new Date();
while (new Date() - startTime < 5) {
console.log("Bデータ取得中");
}
};
setTimeout(() => {
fetchApi();
});
console.log("C:新しい画面の表示");
処理の順番がA→C→B
になったと思います。
このsetTimeout()
というJavascript標準の関数を使用すると、中のコールバック関数の処理を一旦後回しできます。そして下に記述してある処理を優先させて、その処理が完了したらコールバック関数を実行することができます。これが非同期処理
の基本的な考え方になります。
非同期処理
非同期処理は、ある処理を実行させている間に、他の処理を実行させることができる技術です。非同期処理を実行することの最大のメリットは「画面フリーズ(UIスレッドでのブロック発生)」を防ぐことにあります。
ここまでは理解できたでしょうか?
コールバック地獄
現実はより複雑なWebアプリになります。
それでは次に以下のような動作を考えてみましょう。
A:ボタンクリック
B:API-ONEを叩いてデータを加工
C:API-TWOを叩いてデータを加工
D:API-THREEを叩いてデータを加工
E:画面表示
これをコードにするとこのようになると思います。
console.log("A:ボタンクリック");
const fetchApiOne = () => {
const startTime = new Date();
while (new Date() - startTime < 5) {
console.log("API-ONEデータ取得中");
}
console.log("B:API-ONEデータ加工完了");
};
const fetchApiTwo = () => {
const startTime = new Date();
while (new Date() - startTime < 5) {
console.log("API-TWOデータ取得中");
}
console.log("C:API-Twoのデータ加工完了");
};
const fetchApiThree = () => {
const startTime = new Date();
while (new Date() - startTime < 5) {
console.log("API-THREEデータ取得中");
}
console.log("D:API-Threeのデータ加工完了");
};
// ここの部分見にくくないでしょうか?????
setTimeout(() => {
fetchApiOne();
setTimeout(() => {
fetchApiTwo();
setTimeout(() => {
fetchApiThree();
});
});
});
console.log("E:画面表示");
setTimeout()
の箇所ネストが深くなっているのわかりますか?
一般的に「深いネスト」はコードレビューでNGになります。
Promise
非同期処理を行いたい、でもネストを深くしたくない(より読みやすいコードにしたい)
その課題を解決されるために追加された新しい文法があります。
それが「Promise」になります。
はじめは難しいですが、非同期処理
の考え方がしっかりと頭の中で整理できていれば
あとは処理を書き換えているだけの問題なので、不安な方は非同期処理の考え方についてもう一度復習してみてください。
上の記述をPromiseを使った形で書き換えてみます。
console.log("A:ボタンクリック");
const fetchApiOne = () => {
console.log("B:API-Oneを叩く");
const startTime = new Date();
while (new Date() - startTime < 5) {
console.log("API-ONEデータ取得中");
}
console.log("B:API-Oneデータ取得完了");
};
const fetchApiTwo = () => {
console.log("C:API-Twoを叩く");
const startTime = new Date();
while (new Date() - startTime < 5) {
console.log("API-TWOデータ取得中");
}
console.log("C:API-Twoデータ取得完了");
};
const fetchApiThree = () => {
console.log("D:API-Threeを叩く");
const startTime = new Date();
while (new Date() - startTime < 5) {
console.log("API-THREEデータ取得中");
}
console.log("D:API-Threeデータ取得完了");
};
// setTimeout(() => {
// fetchApiOne();
// setTimeout(() => {
// fetchApiTwo();
// setTimeout(() => {
// fetchApiThree();
// });
// });
// });
/* new Promiseをして、その引数に非同期処理をする関数を渡します。
fetchApiOneをした結果をresponseOfFetchApiOneに格納する */
const responseOfFetchApiOne = new Promise((resolve, reject) => {
setTimeout(() => {
// 成功の場合はresolveを使用
resolve(fetchApiOne());
});
});
// fetchApiTwoをした結果をresponseOfFetchApiTwoに格納する
const responseOfFetchApiTwo = new Promise((resolve, reject) => {
setTimeout(() => {
//成功の場合はresolveを使用
resolve(fetchApiTwo());
});
});
// fetchApiThreeをした結果をresponseOfFetchApiThreeに格納する
const responseOfFetchApiThree = new Promise((resolve, reject) => {
setTimeout(() => {
//成功の場合はresolveを使用
resolve(fetchApiThree());
});
});
/*
responseOfFetchApiOne
↓
responseOfFetchApiTwo
↓
responseOfFetchApiThreeの順番に実行される
*/
// ここがthenでつながることでネストが浅くなっている
responseOfFetchApiOne
.then(() => {
return responseOfFetchApiTwo
})
.then(() => {
return responseOfFetchApiThree
})
console.log("E:画面表示");
非常に長いコードになってしまい、初学者の方にはメリットが正直わからないと思いますが、メリットはコードの最後の方のコメントにも書いた通り、ネストの深さが解消されていると思います。
上のコードでは簡略化させるために書かなかったのですが、以下のコードのように.then()
でつなげることによってコードが見やすくなるだけでなく、.catch()
を使用してエラーハンドリングもわかりやすく記述することができます。
instanceOne
.then(() => {
return instanceTwo
})
.then(() => {
return instanceThree
})
.then(() => {
return instanceFour
})
.then(() => {
return instanceFive
})
// このような形でcatch()の中でエラー処理を記述することができる
.catch(() => {
console.log("データ取得中にエラー発生")
})
今回はPromiseの詳細文法を解説する記事ではないので、詳細の書き方や文法については
書籍や公式ドキュメントなどで確認をお願いいたします。
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Promise
今回の記事では「コールバック地獄」がどのような状態のものが、そしてPromiseを使用してどのようにコールバック地獄を解決したかについて概念を理解していただければと思います。
Async/Awaitについて
そして最後の難所です。皆さんもReactを学習するときに確実に習得しなくてはならない文法になります。
async/await
です。
比較的新しい文法でES 2017
においてJavascriptに追加された文法です。
Promiseの記述方法をより簡潔に(同期的に)した書き方になります。
上の書き方をasync awaitを使って書き換えてみます。
console.log("A:ボタンクリック");
const fetchApiOne = () => {
console.log("B:API-Oneを叩く");
const startTime = new Date();
while (new Date() - startTime < 5) {
console.log("API-ONEデータ取得中");
}
console.log("B:API-Oneデータ取得完了");
};
const fetchApiTwo = () => {
console.log("C:API-Twoを叩く");
const startTime = new Date();
while (new Date() - startTime < 5) {
console.log("API-TWOデータ取得中");
}
console.log("C:API-Twoデータ取得完了");
};
const fetchApiThree = () => {
console.log("D:API-Threeを叩く");
const startTime = new Date();
while (new Date() - startTime < 5) {
console.log("API-THREEデータ取得中");
}
console.log("D:API-Threeデータ取得完了");
};
// responseOfFetchApiOneにはPromise型が格納される
const responseOfFetchApiOne = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(fetchApiOne());
});
});
// responseOfFetchApiTwoにはPromise型が格納される
const responseOfFetchApiTwo = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(fetchApiTwo());
});
});
// responseOfFetchApiThreeにはPromise型が格納される
const responseOfFetchApiThree = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(fetchApiThree());
});
});
/* fetchApiOne → fetchApiTwo → fetchApiThreeと処理を順番に繋げるためには、return文を記述しないといけない */
// responseOfFetchApiOne
// .then(() => {
// return responseOfFetchApiTwo
// })
// .then(() => {
// return responseOfFetchApiThree
// })
// 非同期処理を実行するということを明示するために「async」を記述
const fetchApis = async () => {
// 書き慣れたtry-catchを使用してエラーハンドリングが可能
try {
/* return文を書かずに、このように書くだけで、fetchApiOne → fetchApiTwo → fetchApiThreeと順番に処理を行ってくれる。Promise型のインスタンスの前に「await」を記述する */
const resultApiOne = await responseOfFetchApiOne;
const resultApiTwo = await responseOfFetchApiTwo;
const resultApiThree = await responseOfFetchApiThree;
} catch (error) {
console.log(`エラー:${error}`);
}
};
fetchApis();
console.log("E:画面表示");
初心者からするとコード量が多くなっているので混乱する方も多いと思いますが
エンジニアにとって使い慣れているtry-cath構文の中で、非同期処理だからといって特別な書き方をすることなく「同期的に」(awaitを記述すれば、上から順番に処理を実行する)記述をすることができ、コードの見通しが良くなります。
aynsc/awaitの詳細の文法の解説は以下になります。
https://www.sejuku.net/blog/69618
はじめはかなり難しいと思いますが、実際に自分でコードを書き直してみて理解を深めていただければと思います。