LoginSignup
3
2

More than 3 years have passed since last update.

Node JSのコールバックパターン

Last updated at Posted at 2019-08-20

継続渡しスタイル(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 (著), 武舎 広幸 (翻訳), 阿部 和也 (翻訳)

3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2