ここでは非同期処理と同期処理の実例から考えるよくあるアンチパターンについて、理解が深まることで他の仕組みの使い所までわかればいいなと思う。
非同期処理と同期処理の例
コールバックを引数に持つ関数の例
実際の処理と問題点
解決方法
【非同期処理と同期処理の例】
まずは、以下のように簡単に同期処理を非同期処理にすることができる。
出力結果が入れ替わることがわかる。
const check = true
const syncCheck = (check) => {
console.log(check)
}
syncCheck(check)
console.log("結果")
// true
// 結果
// これを非同期にする
const asyncCheck = (check) => {
setTimeout(() => {
console.log(check)
},1000)
}
asyncCheck(check)
console.log("結果")
// 結果
// true
このように関数内の処理は簡単に非同期にすることができるため、コールバック関数の中身は非同期なのか同期なのか実装してみないとわからないという性質があるといえる。
【コールバックを引数に持つ関数の例】
次にコールバックを引数にもつ関数の例を見てみる。
const change = (check, callback) => {
if(check){
console.log(check)
return
}
callback(check)
}
これはcheckがtrueならそのまま出力し、falseならcallbackを呼び出している。
しかしこういったやり方はアンチパターンにつながる。
【実際の処理と問題点】
上のコードをもとに実際の処理をcallbackに渡してみる
ちなみにNode.jsのコールバックによる非同期処理には規約がある。
・コールバックがパラメータの最後であること
・コールバックのパラメータの第一引数がエラーを受け取る変数、第二引数以降が処理の結果を受け取る引数であること
下のコールバック関数はこの規約に沿ったものになっている。
const check = false
change(check, (err, check) => {
setTimeout((check) => {
check = true
console.log(check)
}, 0)
})
console.log("falseの時の呼び出し")
// falseの時の呼び出し
// true
ここではコールバック内でcheckをtrueに変えてから出力している
問題になるのは以下のコードのようにもう一度実行したときに出力する文字列の順番が異なることがあるからだ。
1回目はfalseで非同期に実行される = 先に文字が出力される。2回目はtrueに変わっているので同期的に処理される = 文字は後で出力される。
const check = false
change(check, (err, check) => {
setTimeout((check) => {
check = true
console.log(check)
// ここから2回目を呼び出している
change(check, (err, check) => {
setTimeout((check) => {
check = true
console.log(check)
}, 0)
})
console.log("trueの時の呼び出し")
// 全く同じように呼び出している
}, 0)
})
console.log("falseの時の呼び出し")
// falseの時の呼び出し
// true
// true
// trueの時の呼び出し
これはJavaScriptによく見られるアンチパターンである。場合によって返されるタイミングが変わってしまい、この関数を使うときに非同期か同期かの判断をすることが難しくなる。つまりコールバックを引数に取る関数は非同期か同期かで統一してあげないといけない。
【解決方法】
例えば以下のようにすれば2回目の true のときでも非同期に処理され、順番が変わることがなくなる。
const change = (check, callback) => {
if(check){
setTimeout((check) => {console.log(check)}, 0)
}
callback(check)
}
// 上のコードをそのまま実行すると
// falseの時の呼び出し
// true
// trueの時の呼び出し
// true
上のように同期的な部分を非同期にしたいときは、setTimeout()
でもいいが、
node.jsでは、process.nextTick()
のほうが適している。またこの関数はブラウザ環境では動作しないためブラウザと共有するコードを書いているときは、queueMicrotask()
が適している。
上のコードを修正すると以下のようになる。
const change = (check, callback) => {
if(check){
process.nextTick(() => console.log(check))
}
callback(check)
}
また、この逆で非同期が混ざっている関数を同期的に統一したいときにコールバックヘルが起きる。そういった問題を解決してくれるのがPromiseやasync awaitだ。
【まとめ】
コールバックを引数にとる関数は、その中の処理が非同期に実行されるか同期になるか統一する必要がある。それを助けてくれるのが、process.nextTick()
や Promise , async await
である。