Node.jsでFunction.bind関数で非同期処理がだいぶ書きやすくなった話

皆様お疲れ様です。最近、Node.jsを書くことが多いのですが、Node.jsに標準で入っているFunction.bind関数を利用することで非同期処理がだいぶ書きやすくなった話を書いておきます。

Node.jsでは、性質上、非同期処理主体になるので、Functionのネストが深くなってしまい結構コードが追いにくくなります(自分で)。しかも、あまり深くなっていくと、自分の意図しないタイミングで動いてしまうこともあり、悩ましい。

もちろん、ネストせずに関数の参照で移すこともできなくはないですが、結局JavaScriptのスコープに影響を受けてしまうので、それはそれで組みにくくなるので苦しいところです。

asyncモジュール

こういう悩みで調べていくと、asyncモジュールを使えばいいという話があります。
これをつかうと並列で書けるのですが、若干癖があるので、複数人で開発している時に共有したり、ましてや自分が1年後メンテする際にちょっと苦労しそうな印象があるので、万能というわけではありません。(とはいえ、とっても便利です。)

Function.bind関数

導入が長くなりましたが、Node.jsに標準で入っているFunction.bind関数を利用することで非同期処理がだいぶ書きやすくなった話を書いておきます。

Function.bind関数はDelegate(委譲)のような振る舞いをする機能で、コールバック関数でthisをスコープとして実行できる機能です。

this.test.bind( this );

function test(){
  // thisのスコープとして動く
}

では早速やってみましょう。

サンプルコード

まずサンプルコードです。test.txtを監視して通知を出すプログラムです。watchとreadFileで既にネストがあります。

node_js_bind_pre.js

// 監視するファイル名
this.filename = 'test.txt';
// __________________________________________________________________
// requireを使ったモジュール読み込み
var fs = require('fs');
var path = require('path')
// 対象ファイル
var file = __dirname + path.sep + this.filename;
// 開始タイトル
console.log( '--------------------------------' );
console.log( '[' + this.filename + '] ファイル監視');
// ファイル更新チェックプロセスの開始
if ( process.platform === 'win32' ) {
    // Windows用
    fs.watchFile( file, handlerWatch );
} else {
    fs.watch( file, handlerWatch );
}
// 対象のファイルが変更された後の処理
function handlerWatch(){
    // readFileで対象ファイルを読み込んで表示
    fs.readFile( file , function ( err , data ) {
        // エラー処理
        if( err ) {
            console.error( 'ファイルが存在しません。' );
            process.exit(1);
        }
        // 結果表示
        console.log( '--------------------------------' );
        console.log( 'ファイルが更新されました' );
        // この場合、this.filenameが取得できそうに思えるが
        // スコープではないので【ファイル内容 undefined】となる
        console.log( '【ファイル内容 ' + this.filename + '】' );
        console.log( data.toString() );
    });
}

簡単なファイル監視のコードなので別にネストさせてもいいですが、今回はサンプルということで書き換えてみます。

注目するところとしては

  • handlerWatchコールバックにしている理由はwatchFileとwatchの両方の処理の結果が共通のコールバックなのでそのようにしています。
  • handlerWatchコールバック内でreadFileをファイルを呼ぶときはスコープは意識せずvarで指定された変数なので上手く動作します。
  • readFile内のコールバックにあるthis.filenameはthisがメインの階層のスコープでないのでundefinedになります。
--------------------------------
[test.txt] ファイル監視
--------------------------------
ファイルが更新されました
【ファイル内容 undefined】
TEST1

といったところです。

bindで書き換え

さてこれをbindで書き換えてみます。

node_js_bind.js

// 監視するファイル名
this.filename = 'test.txt';
// __________________________________________________________________
// requireを使ったモジュール読み込み
var fs = require('fs');
var path = require('path')
// 対象ファイル
var file = __dirname + path.sep + this.filename;
// 開始タイトル
console.log( '--------------------------------' );
console.log( '[' + this.filename + '] ファイル監視');
// ファイル更新チェックプロセスの開始
if ( process.platform === 'win32' ) {
    // Windows用
    fs.watchFile( file, handlerWatch.bind(this) );
} else {
    fs.watch( file, handlerWatch.bind(this) );
}
// 対象のファイルが変更された後の処理
function handlerWatch(){
    // readFileで対象ファイルを読み込んで表示
    fs.readFile( file , callbackReadFile.bind(this) );
}

// 移植:readFileで対象ファイルを読み込んで表示
function callbackReadFile ( err , data ) {
    // エラー処理
    if( err ) {
        console.error( 'ファイルが存在しません。' );
        process.exit(1);
    }
    // 結果表示
    console.log( '--------------------------------' );
    console.log( 'ファイルが更新されました' );
    // この場合、スコープが一致するので、this.filenameが取得できる。
    console.log( '【ファイル内容 ' + this.filename + '】' );
    console.log( data.toString() );
}

このようになります。先ほどのthis.filenameがundefinedとなるところも

--------------------------------
[test.txt] ファイル監視
--------------------------------
ファイルが更新されました
【ファイル内容 test.txt】
TEST1

となり、thisでスコープが通ります。

このようにbind関数をつかうとネストが並列でかけるようになり、明示的にスコープを与えられるので比較的追いやすくなります。

またbined関数はNode.jsに標準でついています。ということは、追加モジュールが必要なく素の状態で使えるのがいいところです。

ActionScriptのDelegateのようなコードで書け、ある程度AS3のクラスのような風に書けるのも気に入っています。

おわりに

いかがでしたでしょうか。

この辺りの解決方法は色々あると思いますが、あくまで一例として挙げました。参考になれば幸いです。

今回もファイル一式置いておきます。

ファイル一式ダウンロード:node_js_bind.zip

それでは、よきNode.js Lifeを。