11
10

More than 5 years have passed since last update.

Promiseでディレクトリの中のファイルごとに行う非同期再帰呼び出しをやめる

Last updated at Posted at 2016-04-16

概要

JavaScript Promiseの本4.8 Promiseによる逐次処理 が読んでもピンとこなかったため、逐次処理で再帰呼び出しに頼ってきていましたが、自分でコードを書いてみてやっと理解できてきたのでまとめておきます。

前提

  • 動作確認は node.js ver.5.7.0 で行っています。
  • Promise の最低限の知識は必要ですが、完璧に理解できている必要はありません(自分も理解できていません)。

再帰呼び出しサンプル

fs.readdir()でルートディレクトリの各ファイルについて処理を行う簡単なサンプルを以下に示します。
処理はダミーとして1秒以内の乱数によるタイムアウトとしています。

ポイント

再帰呼び出しは、関数側で自身を呼び出します。そのため以下の注意が必要です。

  1. 関数は再帰呼び出しのため配列とインデックスを必要とする。
  2. 次の処理を行う時にインデックスを+1して再帰呼び出しをする。
  3. 最後の処理を再帰ルーチンの中で行う必要がある。

サンプルコード

sample1.js
'use strict';
const fs = require('fs');
/*
 * ポイント1. 引数に配列(files)とインデックス(index)が必要
 */
function recursive(name, index, files, callback) {
    if (index == files.length) {
        // ポイント3. 最後の処理
        callback();
        return;
    }
    const timeout = Math.floor(Math.random()*1000);
    setTimeout(() => {
        console.log('Recursive:Done:' + files[index]);
        // ポイント2: インデックスを増やして再帰呼び出し
        recursive(files[index+1], index+1, files, callback);
    }, timeout);
}

fs.readdir('/', (err, files) => {
    console.log('Start recursive');
    recursive(files[0], 0, files, () => {
        console.log('Recursive:All done');
    });
});

Promiseで再帰呼び出しをやめる

sampl1.js を修正しました。再帰呼び出しをやめることにより、再帰呼び出しに必要だったポイントは消え、サブルーチン側はサブルーチンの処理に専念できます。

ポイント

  1. 関数は再帰呼び出しのため配列とインデックスを必要とする。
  2. 次の処理を行う時にインデックスを+1して再帰呼び出しをする。
  3. 最後の処理を再帰ルーチンの中で行う必要がある。
  4. 関数はPromiseを返すようにする。
  5. 処理が終わった時にはresolve()を呼び出す。
  6. 呼び出し側はPromise.then()を連結していく。

サンプルコード

sample2.js
'use strict';
const fs = require('fs');

/*
 * 関数の引数はnameだけとなった。
 */
function sub(name) {
    // ポイント4: Promiseを作成する。
    return new Promise((resolve, reject) => {
        const timeout = Math.floor(Math.random()*1000);
        setTimeout(() => {
            console.log('Promise:Done:' + name);
            // ポイント5: 再帰呼び出しの代わりにresolve()を呼び出す。
            resolve();
        }, timeout);
    });
}
fs.readdir('/', (err, files) => {
    // Promise Array.prototype.forEach()呼び出し
    console.log('Start promise(forEach)');
    // Promiseの初期値を設定
    let p = Promise.resolve();
    files.forEach((name) => {
        // ポイント6: 呼び出し側はPromiseを連結する。
        p = p.then(() => {
            return sub(name);
        });
    });
    p.then(() => {
        console.log('promise(forEach):All done');
    });
});

Array.prototype.reduce()を使う

Array.prototype.reduce() を使うと呼び出し側をもう少し簡潔に書けます。

ポイント

  1. 関数は再帰呼び出しのため配列とインデックスを必要とする。
  2. 次の処理を行う時にインデックスを+1して再帰呼び出しをする。
  3. 最後の処理を再帰ルーチンの中で行う必要がある。
  4. 関数はPromiseを返すようにする。 (sample2.js 参照)
  5. 処理が終わった時にはresolve()を呼び出す。 (sample2.js 参照)
  6. 呼び出し側はPromise.then()を連結していく。 (sample2.js 参照)
  7. 最初のPromiseの初期化はreduce()の3番目の引数で行う。
  8. 処理終了時の待ちはreduce()で返されるPromiseを使う。

サンプルコード

sample3.js
'use strict';
const fs = require('fs');

/*
 * sample2.jsと同じ
 */
function sub(name) {
    // Promiseを返す。
    return new Promise((resolve, reject) => {
        const timeout = Math.floor(Math.random()*1000);
        setTimeout(() => {
            console.log('Promise:Done:' + name);
            resolve(); // 再帰呼び出しの代わりにresolve()
        }, timeout);
    });
}
fs.readdir('/', (err, files) => {
    console.log('Start promise(reduce)');
    files.reduce((promise, name) => {
        return promise.then(() => {
            return sub(name);
        });
        // ポイント7: 最初のPromiseの初期化はreduce()の3番目の引数で行う。
    }, Promise.resolve()).then(() => {
        // ポイント8: 処理終了時の待ちはreduce()で返されるPromiseを使う。
        console.log('promise(reduce):All done');
    });
});

バリエーション

呼び出す関数に引数が必要ない場合

呼び出す関数がPromiseを返す非同期関数で引数が必要ない場合はthen()の中に関数を作る必要はありませんが、関数を渡すために()を外す必要があります。

noargs.js
function subWithoutArgs() { // 引数が必要ない場合
  :
}
       :
    p = p.then(subWithoutArgs);

終わりに

非同期の再帰呼び出しというと、Promiseをちゃんと理解した上で、ロジックを大幅に書き直さなきゃならないんじゃないかと勝手に思い込んで手を出せませんでした。
ただ、こうしてまとめてみると呼び出し側をPromiseで括れば、自分の求める範囲では大した話ではないことがわかりました。
自分と同じ思いをしている方がいて、この記事が理解の助けになれば幸いです。

11
10
4

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
11
10