NodeJS 異步操作:擺脫‘Callback hell’初談

衆所周知,NodeJS具有的單線程,事件驅動,異步非阻塞IO模型,使得其在IO密集型程序,尤其是大型的Web服務中佔有很大的優勢。

下面就來談談幾種NodeJS異步回調的實現。
最常規的一種是:

import fs from 'fs';
fs.readFile(__dirname + '/a.txt', 'utf-8', (err, data) => {
    if(!err){
        console.log(data);
        console.log('Finish reading');
    } else console.log(err);
})
console.log('Start reading');

輸出:

Start reading
aaa
Finish reading

如果用戶想自定義回調函數代替 console.log(data)
可以再封裝一層,將callback作爲回調函數傳入內部:

import fs from 'fs';
const readFile = (path, callback) =>{
    fs.readFile(__dirname + path, 'utf-8', (err, data) => {
        if(!err){
            callback(data);
            console.log('Finish reading');
        } else console.log(err);
    })
}
readFile('/a.txt', (data) => { console.log(`The content of file "a.txt" is: ${data}`)});
console.log('Start reading');

輸出:

Start reading
The content of file "a.txt" is aaa
Finish reading

NodeJS將幾乎所有的庫函數實現爲如下形式的異步回調機制

asyncFunc(arg0, arg1, ... (err, data) => {
    if(!err) callback(data);
})

此時,假設我們以readFile函數爲例,它實現了異步讀取本地文件並在讀取到內容後回調的功能。假如我們已經獲取了當前路徑某個文件夾下的3個文件[a.txt, b.txt, c.txt],需將順次遍歷和打印其內容,首先想到的是暴力和醜陋的多層回調方法如下:

import fs from 'fs';
const read3Files = (callback) => {
    fs.readFile(__dirname + '/a.txt', 'utf-8', (err, data) => {
    if(!err){
        callback('a.txt', data);
        fs.readFile(__dirname + '/b.txt', 'utf-8', (err, data) => {
            if(!err){   
                callback('b.txt', data);
                fs.readFile(__dirname + '/c.txt', 'utf-8', (err, data) => {
                    if(!err){                   
                        callback('c.txt', data);
                        console.log(`Done...`);
                    } else console.log(err);
                })
            } else console.log(err);
        })
    } else console.log(err);
})
}
read3Files( (file, data) => { console.log(`The content of file ${file} is: ${data}`)});
console.log('Start reading');

輸出:

Start reading
The content of file a.txt is: aaa
The content of file b.txt is: bbb
The content of file c.txt is: ccc
Done...

這就是臭名昭著的回調地獄了,回調函數層層嵌套,不僅缺乏美感,而且給Debug造成很大的困難。

而且這麼做有個很大的弊端,讀取的文件是硬編碼的,如果給的文件列表數個很長的數組,就無法這麼做了。

觀察到上述代碼的特性,我們可以將該其寫成遞歸函數的形式如下,函數細節不再贅述,在當前文件夾下有5個txt文件如下,代碼採用遞歸形式依次讀取其內容。

import fs from 'fs';
let fsList = ['/a.txt','/b.txt','/c.txt','/d.txt','/e.txt'];
const readFileList = (fsList, i, callback, err, data) => {
    if(i > fsList.length) {
        console.log(`Done...`);
        return;
    }
    if(err) {
        console.log(err);
        return;
    } else if(data){
        callback(fsList, i - 1, data);
    }
    fs.readFile(__dirname + fsList[i], 'utf-8', (err, data) => {
        return readFileList(fsList, i + 1, callback, err, data);
    });
}
readFileList(fsList, 0, (fsList, i, data) => {console.log(`The content of file ${fsList[i]} is: ${data}`)}, null, null);

輸出:

Start reading
The content of file a.txt is: aaa
The content of file b.txt is: bbb
The content of file c.txt is: ccc
The content of file /d.txt is: ddd
The content of file /e.txt is: eee
Done...

這種方法看似簡便了許多,但我們不想將時間浪費在自己實現遞歸函數這種難以Debug又容易產生各種錯誤的工作上,那麼有沒有
更爲優雅的實現方式呢,在這裏我們引入ES6和ES7的新特性,PromiseAsync, Await關鍵詞。

ES6 原生提供了 Promise 對象。所謂 Promise 對象,就是代表了某個未來纔會知道結果的事件(通常是一個異步操作),並且這個事件提供統一的 API,可供進一步處理。

有了 Promise 對象,就可以將異步操作以同步操作的流程表達出來,避免了層層嵌套的回調函數。此外,Promise 對象提供的接口,使得控制異步操作更加容易。

let promise = new Promise((resolve, reject) => {
  if (/* 異步操作成功 */){
    resolve(value);
  } else {
    reject(err);
  }
});

promise.then(value => {
  // success
}.catch(err => {
  // fail
})

因此對於readFile這個函數,或是你需要在程序中實現‘整體異步,內部鏈式調用‘的異步函數,我們可以將其封裝爲一個Promise對象,並將自定義callback傳入then和catch函數:

import fs from 'fs';
const readFile2 = (path) => {
    return new Promise((resolve, reject) => {
        fs.readFile(__dirname + path, 'utf-8', (err, data) => {
            if(!err) resolve(data);
            else reject(err);
        })
    })
}
readFile2('/a.txt').then(data => console.log(data)).catch(err => console.log(err));

##使用鏈式調用讀取多個文件:

readFile2('/a.txt').then(data => {
    console.log(data);
    return readFile2('/b.txt');
}).then(data => {
    console.log(data);
    return readFile2('/c.txt');
}).then(data => {
    console.log(data);
    return readFile2('/d.txt');
}).then(data => {
    console.log(data);
    console.log(`Done...`);
}).catch(err => console.log(err));

輸出:

Start reading
aaa
bbb
ccc
ddd
Done...

封裝Promise及鏈式調用的方法看上去很優雅,但其缺點也是硬編碼,無法實現讀取一個列表中的文件,
筆者暫時還沒想到用遞歸調用的形式實現上述的鏈式函數。

在下一節中,筆者將引入 Async 和 Await 關鍵字來實現順次以及非順次讀取一個FileList中的文件內容。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章