概要
Node.jsといえば非同期やcallbackで有名ですが、1つの関数の中で非同期な処理と同期的な処理が混ざってしまうと期待しない挙動になることがあるので注意しましょう、という話です。
環境
- Node.js 8.9.0
基本的な方針
- 非同期な関数は全て非同期処理でまとめる
- 同期的な関数は全て同期的な処理でまとめる
です。
非同期な関数とは
でまとめましたが、
- Timer(setTimeout, setInterval)
- I/O event
- setImmediate
- process.nextTick
- Promise
などです。
具体的なコード
console.log('start'); setTimeout(() => { console.log('setTimeout') }, 0); console.log('end');
こういうやつですね。
結果
非同期処理なのでscriptが完了してから実行されます。
start
end
setTimeout
同期的な関数とは
上記のような非同期ではないケースのことです。
具体的なコード
console.log('start'); function showName(name) { console.log(name); } showName('foo'); console.log('end');
結果
同期処理なので順に実行されます。
start
foo
end
callbackで書いても同期的なケース
callbackを見ると反射的に非同期が浮かびますが、実際は中身を見てみないと分かりません。 例えば以下はcallbackを使っていますが、結局のところ同期処理なので実行順も書いた通りになります。
console.log('start'); function someCallback(name, callback) { callback(name); } someCallback('foo', (arg) => { console.log(arg); }); console.log('end');
結果
同期処理なので順に実行されます。
start
foo
end
同期関数と非同期関数が混ざるというのは
例えば以下のようなキャッシュ機能を持つファイル読み込み関数があるとします。
if文でキャッシュの処理を分けています。
const cache = {}; function readFile(fileName, callback) { if (cache[filename]) { return callback(null, cache[filename]) } fs.readFile(fileName, (err, fileContent) => { if (err) return callback(err); cache[fileName] = fileContent; callback(null, fileContent); }); }
呼び出す処理は以下の通りです。
function main(){ readFile('myfile.txt', (err, result) => { console.log('file read complete'); }); console.log('file read initiated') }
どんな問題が起きるのか?
先程のコードを実行すると以下の結果になります。
# 1回目 file read initiated file read complete # 2回目 file read complete file read initiated
つまり実行タイミングによって処理の順番が変わってしまう、という問題が起きます。
どこが原因か?
非同期処理である
fs.readFile(fileName, (err, fileContent) => { if (err) return callback(err); ...
と、同期処理である
if (cache[filename]) { return callback(null, cache[filename]) }
のcallbackが同じように実行されている点です。
どうすれば解決するのか
解決方法ですが、主に2通りあります。
同期関数で揃える
非同期関数であるfs.readFile
の代わりにfs.readFileSync
を使うことで、全て同期処理で統一します。
これによって以下のように順番が保証されます。
# 1回目 file read initiated file read complete # 2回目 file read initiated file read complete
非同期関数で揃える
同期関数である
return callback(null, cache[filename])
を以下のようにprocess.nextTick()
を使うことで全て非同期にします。
return process.nextTick(() => callback(null, cache[filename]));
これによって以下のように順番が保証されます。
# 1回目 file read initiated file read complete # 2回目 file read initiated file read complete
まとめ
1つの関数の中では非同期な処理と同期的な処理は混ぜないように、という話でした。
特にifの分岐で漏れることがあるので注意してください。