衆所周知,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的新特性,Promise
和Async, 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中的文件內容。