継続渡しスタイル(continuation-passing style : CPS)
JavaScriptにおいてコールバック関数とは、ある関数を呼び出す時に、引数として指定する関数で、FuncAの処理が完了した時にFuncAの結果を通知するために起動される関数のことを指します。
同期的継続渡しスタイル
test.js
function add(a, b, callback) {
callback(a + b)
}
console.log('before')
add(1, 2, result => console.log('Result : ' + result))
console.log('after')
結果
example.sh
$ node test.js
before
Result : 3
after
非同期CPS
test.js
function addAsync(a, b, callback) {
setTimeout(() => callback(a + b), 100)
}
console.log('before')
add(1, 2, result => console.log('Result : ' + result))
console.log('after')
結果
example.sh
$ node test.js
before
after
Result : 3
継続渡しではないコールバック
test.js
const result = [1, 5, 7].map(element => element - 1)
console.log(result) // [0, 4, 6]
ちなみに以下のことはダイレクトスタイル(Direct Style:DS)と呼びます。
example.js
function add(a, b) {
return a + b
}
console.log(add(1,2)) // 3
同期処理か非同期処理か
どっちでもいいですが、まず避けなければならないのは「一貫性がないAPI」です。
同期と非同期の混在
もっとも危険なのは、ある条件には同期処理、ある条件には非同期処理を行う関数です。
以下のコードは一貫性がないコードの例です。
test.js
const fs = require('fs')
const cache = {}
function inconsistentRead(filename, callback) {
if(cache[filename]) {
callback(cache[filename]) // cacheにデータがある場合、同期的に実行される。
} else {
// cacheにデータがない場合、ひい同期関数fs.readFile()を呼び出す。
fs.readFile(filename, 'utf8', (err, data) => {
cache[filename] = data
callback(data)
}
}
}
混在がもたらす問題
test.js
const fs = require('fs')
const cache = {}
function inconsistentRead(filename, callback) {
if(cache[filename]) {
callback(cache[filename]) // cacheにデータがある場合、同期的に実行される。
} else {
// cacheにデータがない場合、ひい同期関数fs.readFile()を呼び出す。
fs.readFile(filename, 'utf8', (err, data) => {
cache[filename] = data
callback(data)
})
}
}
function createFileReader(filename) {
const listeners = []
inconsistentRead(filename, value => {
listeners.forEach(listener => listener(value))
})
return {
onDataReady: listener => listeners.push(listener)
}
}
const reader1 = createFileReader('data.txt')
reader1.onDataReady(data => {
console.log('First call data: ' + data)
// しばらくしてから同じファイルを呼び出す
const reader2 = createFileReader('data.txt')
reader2.onDataReady(data => {
console.log('Second call data: ' + data)
})
})
結果
example.sh
$ node test.js
First call data: some data
解決策1同期APIの利用
同期関数を使えば解決できます。
test.js
const fs = require('fs')
const cache = {}
function consistentReadSync(filename) {
if (cache[filename]) {
return cache[filename]
} else {
cache[filename] = fs.readFileSync(filename, 'utf8') // 同期関数使用
return cache[filename]
}
}
しかし、以下の留意点があります。
・ある機能に関して常に同期バージョウンが用意されているとは限らない
・他のリクエストは処理待ちとなるため、全体的にパフォーマンスが落ちる。
解決策2遅延実行
同期的なコールバックが「将来」起動されるようにスケジュールする。
test.js
const fs = require('fs')
const cache = {}
function consistentReadAsync(filename) {
if (cache[filename]) {
process.nextTick(() => callback(cache[filename]))
} else {
fs.readFile(filename, 'utf8', (err, data) => {
cache[filename] = data
callback(data)
}
}
}
Node.jsのコールバック
エラーの伝播
test.js
const fs = require('fs')
function readJSON(filename, callback) {
fs.readFile(filename, 'utf8', (err, data) => {
let parsed
if (err) {
return callback(err)
}
try {
parsed = JSON.parse(data)
} catch (err) {
return callback(err)
}
callback(null, parsed)
})
}
キャッチされない例外
test.js
const fs = require('fs')
function readJSONThrows(filename, callback) {
fs.readFile(filename, 'utf8', (err, data) => {
let parsed
if (err) {
return callback(err)
}
callback(null, JSON.parse(data))
})
}
try {
readJSONThrows('nonjson.txt', err => {
if (err) { console.log(err) } else { JSON.stringify(json) }
})
} catch(err) {
console.log('こうしてもキャッチはできません。')
}
上記の場合、キャッチされません。なぜなら、readJSONThrowsを呼び出すスタックとコールバックを呼び出すスタックがことなるからです。
これは下記のようにコードを作ったらキャッチされます。
test.js
readJSONThrows('nonjson.txt', err => {
if (err) { console.log(err) } else { JSON.stringify(json) }
})
process.on('uncaughtException', (err) => {
console.error('ここでキャッチ')
process.exit(1) // エラーコード1で終了。これがないと実行を継続する。
})
参考文献
Node.jsデザインパターン 第2版 - Mario Casciaro (著), Luciano Mammino (著), 武舎 広幸 (翻訳), 阿部 和也 (翻訳)