能書き
JSの重要な概念の一つに同期・非同期処理というものがあります。と言ってもこれを理解したのは最近の事で、偉そうな事は言えません。そもそも既に枯れたテーマなのでここより詳しい記事は山ほどあると思います。ただJSを使って開発する時に、気づけば何度も同じことを調べ直していたので、自分にとって使いやすい備忘録としてまとめてみたいと思います。温故知新という奴です。
同期処理とは
まず次のコードを実行してみます。
// HTTPSリクエスト設定
const request = require('request');
var options = {
url: 'https://momozo.tech',
method: 'GET',
port: 443
}
// 標準出力用文字列設定
const fs = require('fs');
var data1 = fs.readFileSync('data1.txt', 'utf-8');
var data2 = fs.readFileSync('data2.txt', 'utf-8');
var data3 = fs.readFileSync('data3.txt', 'utf-8');
var data4 = fs.readFileSync('data4.txt', 'utf-8');
var data5 = fs.readFileSync('data5.txt', 'utf-8');
// 出力1
console.log(data1 + data2 + data3 + data4);
// HTTPSリクエスト
request(options, function(error, res, body) {
console.log(body);
});
// 出力2
console.log(data5);
さて、どうなるでしょうか。プログラムは基本的に上から下に実行されるのでdata1 data2 data3 data4
の後にhttpリクエスト結果が続き、最後にdata5
が出力されるでしょうか。それとも、こんな事を聞くくらいなので違う結果になるのでしょうか。
data1
data2
data3
data4
data5
<!DOCTYPE html>
<html lang="ja" class="no-js">
<body>
</body>
</html>
※一部抜粋した物です。
data5
がhttpsリクエストのレスポンスを待たずに、先に出力されています。つまり非同期処理となっております。JSはシングルスレッドがうんちゃらで、標準では同期処理が出来ません。自分も初めはこれが分かっておらず、問題の原因究明が難航したことが何度もありました。但し標準では出来ないだけで、きちんとコードを書けば同期処理を実現する事は出来ます。今回の目標は順番通り同期的に処理を実行するコードの書き方をまとめる事です。
同期処理を実現
同期処理が出来ないとどんな時に困るのでしょうか。例えば関数Aでデータを抽出して、それを元に関数Bを実行する場合を考えてみます。関数Aでデータを抽出する前に関数Bが実行されると問題が起こるのは想像に難くないかと思います。
コールバック地獄という言葉を耳にした事はあるでしょうか。上記の内容をJSで実現しようとして煩雑になったコードを揶揄した言葉です。これについても後学の役に立つので機会が在ればまとめたいと思いますが、ボリュームが多いのと本筋から少しだけ逸れるので割愛します。要は関数Aが実行された後に関数Bを実行したい時に、関数Aの引数に関数Bを渡すことをコールバックと言うのです。そしてこれこそが同期処理の肝になります。
Promise
とthen
を使う事で非同期処理を制御する事が出来ます。ひいてはコールバック関数を可読性を保ちながら記述する事が可能です。早速実際のコードを見てみます。
// HTTPSリクエスト設定
const request = require('request');
var options = {
url: 'https://momozo.tech',
method: 'GET',
port: 443
}
// 標準出力用文字列設定
const fs = require('fs');
var data1 = fs.readFileSync('data1.txt', 'utf-8');
var data2 = fs.readFileSync('data2.txt', 'utf-8');
var data3 = fs.readFileSync('data3.txt', 'utf-8');
var data4 = fs.readFileSync('data4.txt', 'utf-8');
var data5 = fs.readFileSync('data5.txt', 'utf-8');
function asyncfunc() {
return new Promise(function (resolve, reject) {
resolve(data1 + data2 + data3 + data4);
});
}
asyncfunc()
.then(function(data) {
console.log(data);
return new Promise(function (resolve, reject) {
var res = request(options, function(err, res, body) {
resolve(body);
});
})
})
.then(function(data) {
console.log(data);
console.log(data5);
})
.catch(function(error) {
console.log(error);
});
17行目以降に注目して下さい。Promise
でasyncfunc()
のコールバック関数を登録しています。resolve
はasyncfunc()
内の処理が成功した時に、reject
は失敗した時にそれぞれ呼ばれます。そしてresolve
の引数に文字列を渡しているので、asyncfunc()
の後続のthen
で定義されたコールバック関数に引き渡す事が出来ます。
2つ目のthen
に関しても同様に読む事が出来ます。今度はresolve
の引数にレスポンス結果を渡していて後続のコールバック関数がそれを受け取って出力します。
最後のcatch
はそれまでのthen
でこけた場合のエラーメッセージをまとめて出力します。
それでは実行してみます。
data1
data2
data3
data4
<!DOCTYPE html>
<html lang="ja" class="no-js">
<body>
</body>
</html>
※一部抜粋した物です。
data5
同期的に処理が走る筈です。
もっと簡単に
実はもっと簡単に実現する方法もあります。sync-request
モジュールを使えば、勝手に同期処理となります。
const fs = require('fs');
var data1 = fs.readFileSync('data1.txt', 'utf-8');
var data2 = fs.readFileSync('data2.txt', 'utf-8');
var data3 = fs.readFileSync('data3.txt', 'utf-8');
var data4 = fs.readFileSync('data4.txt', 'utf-8');
var data5 = fs.readFileSync('data5.txt', 'utf-8');
const request = require('sync-request');
console.log(data1 + data2 + data3 + data4);
var res = request('GET', 'https://momozo.tech/');
console.log(res.getBody('utf8'));
console.log(data5);
実行結果。
data1
data2
data3
data4
<!DOCTYPE html>
<html lang="ja" class="no-js">
<body>
</body>
</html>
※一部抜粋した物です。
data5
そもそも同期的なモジュールを使うという方法もあるのです。ですがコールバック関数は逃れられない概念ですので、しっかり把握しておくのが吉と存じます。それからreadFileSync
やgetBody()
で文字コード(utf8)を指定しているのは、指定しないとバイナリファイルとして開こうとするからです。JavaScriptは今一番身に付けたい開発言語ですが、思わぬ落とし穴が多いです。
以上。