コールバックモードの欠点とPromise


ES6に先立って、非同期処理における動作順序を保証するためにerror−firstコールバックモードが使用される.node.jsにおいて、fsモジュールを使用してファイルを読み取る際に使用されるreadFile()関数は、error−firstコールバックモードを使用する良い例である.
しかし使い続けて多くの欠点を発見し、それを解決するためにPromiseが誕生した.
いったい何が問題なのか、error-first callbackモードをPromiseモードに変更する方法もあります!🙂
callback hell
コールバックhellは、非同期処理が必要な操作順序を保証しようとしたときに発生する.
例えばtext 1.txt -> text_2.txt -> text_3.txt順にファイルの内容を出力します.
const fs = require('fs');

fs.readFile('./text_1.txt', (err, data) => {
    if(err) { 
        console.error(err); 
    }
    console.log(data.toString());
});

fs.readFile('./text_2.txt', (err, data) => {
    if(err) { 
        console.error(err); 
    }
    console.log(data.toString());
});

fs.readFile('./text_3.txt', (err, data) => {
    if(err) { 
        console.error(err); 
    }
    console.log(data.toString());
});
実行結果
1. text_1.txt content!
2. text_3.txt content!
3. text_2.txt content!
非同期処理は同期ストリームに従わないため、非同期操作の実行順序は保証されません.😥
ファイルの容量がそれぞれ異なると、より予想外の結果が得られる可能性があります.
今回はコードを修正して、順番に実行させます!
const fs = require('fs');

fs.readFile('./text_1.txt', (err, data) => {
    if(err) { 
        console.error(err); 
    }
    console.log(data.toString());

    fs.readFile('./text_2.txt', (err, data) => {
        if(err) { 
            console.error(err); 
        }
        console.log(data.toString());

        fs.readFile('./text_3.txt', (err, data) => {
            if(err) { 
                console.error(err); 
            }
            console.log(data.toString());
        });
    });
});
実行結果
1. text_1.txt content!
2. text_2.txt content!
3. text_3.txt content!
上述したように、コードを変更することで、非同期動作の順序を保証することができる.しかし、順番に実行するファイルが10個を超えるとしたら?🤔
このようにerror-first callbackモードを使用すると、コードのindentが深まり、現在はデータを出力する簡単な操作ですが、インポートしたデータを四半期ごとに異なる操作を行うと、その可読性が低下します.
それ以外に,例外処理を行うためには,try...catch構文を各callback関数に追加する必要があり,多くの欠点が存在し,これらの欠点を解決するためにPromiseが出現した.
Promise
Promiseはerror-firstコールバックモードの多くの欠点を補うために生まれた機能である.
しかし、これはコールバックを全く使用しないのではなく、非同期処理のためのコールバックに現れる可能性のある様々な欠点を補う機能である.
Promiseの特徴は,非同期処理を実行できることであるが,結果を直接コールバック関数として渡すのではなく,後で処理できることである.
また、Promiseは、new Promise()コンストラクション関数によってPromiseオブジェクトを作成して処理するので、Promiseオブジェクトを他の関数に渡して処理してもよい.
では、Promiseについて詳しくご紹介しましょう.😆
Promiseの状態
Promiseには、次の3つの状態があります.

  • pending
    Promiseの作成からfulfilledまたはrejectedのステータス

  • fulfilled
    Promiseでresolve()関数が呼び出されると、fulfilled状態になります.

  • rejected
    Promiseでreject()関数が呼び出されると、rejected状態になります.
  • その後、resolve()またはreject()の関数が呼び出されると、このPromiseは解析済みPromiseと呼ばれ、解析済みPromiseがresolve()またはreject()を再び呼び出すとしても、その状態は変化しない.
    要するに、Promiseは最終的にfulfilledまたはrejectedのいずれかの状態にすぎない.
    Promiseオブジェクトの利用
    Promiseが登場してから、error-firstコールバックモードを使用する関数はPromiseをサポートするようになりました.
    ただし、Promiseオブジェクトを返さない関数はnew Promise()コンストラクション関数を使用してPromiseオブジェクトを使用することもできるので、心配することはありません.fs.readFile()関数をPromiseオブジェクトに変換する例で、Promiseの使い方を理解します!🙂
    const fs = require('fs');
    
    getText('./text_1.txt')
    .then(data => console.log(data)) // 파일 내용이 있을 경우 실행
    .catch(err => console.error(err)); // 파일 내용이 없거나 파일을 불러오지 못 한 경우 실행
    
    function getText(path) {
        return new Promise((resolve, reject) => {
            fs.readFile(path, (err, data) => {
                if(err || data.toString() === '') 
                  	reject(new Error('no contents'));
                resolve(data.toString());
            })
        });
    }
    (fsモジュールではPromiseがサポートされていましたが、error-firstコールバックモード関数をPromiseオブジェクトとして作成する方法を示すために、わざわざこのように例を書きました.)
    これで、getText()関数を呼び出すとPromiseオブジェクトが返され、Promiseオブジェクトのパラメータを表示すると、resolveおよびrejectを持つcallback関数を使用して非同期処理するタスクを囲むことがわかります.resolveおよびrejectもコールバック関数であり、関数が正常に動作している場合はresolve()を呼び出し、失敗した場合は条件に従ってreject()を呼び出す.
    そして、戻るPromiseオブジェクトのthen()メソッドを用いてresolve()を処理し、catch()メソッドを用いてreject()を処理することができる.then()メソッドの2番目のパラメータもreject()を扱うことができますが、これではthen()コールバックで発生する例外を見つけることができないので、catch()メソッドを使いましょう!😣
    このような利点はcallbackの呼び出しを2回防止できることであるが,Promiseの真の価値はCheningにある.
    Promise chaining
    Promiseチェーンがないと仮定し、Promiseを使用してcallback hellが発生する可能性のあるコードを作成します.
    比較のためにcallback hellで使用する例は同じです!
    const fs = require('fs');
    
    getText('./text_1.txt').then(data => {
        console.log(data);
        getText('./text_2.txt').then(data => {
            console.log(data);
            getText('./text_3.txt').then(data => {
                console.log(data);
            }, (err) => console.error(err))
        }, (err) => console.error(err))
    }, (err) => console.error(err));
    
    function getText(path) {
        return new Promise((resolve, reject) => {
            fs.readFile(path, (err, data) => {
                if(data.toString() === '') reject(new Error('err'));
                resolve(data.toString());
            })
        });
    }
    Promiseを使用していますが、indentがerror-firstコールバックモードを使用しているのとあまり変わらないように見えます...😥
    しかし,Promiseチェーンを用いて非常に簡潔なコードを記述することができる.
    const fs = require('fs');
    
    getText('./text_1.txt')
    .then(data => { console.log(data); return getText('./text_2.txt');})
    .then(data => { console.log(data); return getText('./text_3.txt');})
    .then(data => console.log(data))
    .catch(err => console.error(err));
    
    function getText(path) {
        return new Promise((resolve, reject) => {
            fs.readFile(path, (err, data) => {
                if(data.toString() === '') reject(new Error('err'));
                resolve(data.toString());
            })
        });
    }
    Promiseもオブジェクトであるため、then()関数の戻り値で別のPromiseオブジェクトを返し、コードを簡潔にします.
    また,複数の非同期タスクに対するreject()処理もPromiseチェーンを用いて1つのcatch()で処理できる!
    このようにPromiseを用いることでerror−firstコールバックモードの多くの欠点を解決できる.JavaScriptの特性のため、非同期処理はほとんど必要であり、error−firstコールバックに基づくAPIの多くはPromiseに基づいて再構築されているため、Promiseオブジェクトの正確な理解が必要である.
    不足や間違いがあれば、指摘してください.ありがとうございます.🤗
    参考資料
    Using promises - JavaScript | MDNhttps://developer.mozilla.org/ko/docs/Web/JavaScript/Guide/Using_promises
    Promise()作成者-JavaScript|MDNhttps://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Promise/Promise
    Promise - JavaScript | MDNhttps://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Promise