在上一節中我們介紹了NodeJS基礎的異步回調實現方法,實現了異步的遞歸函數擺脫了‘回調地獄’。最後引入了Promise 特性將異步回調實現地更加優雅。
然而基於Promise的鏈式調用方法目前看似只能實現硬編碼。基於上一節的問題,我們有一個文件列表,需要順次讀取其中的內容並打印。在這裏我們引入es7的 Async 和 Await 關鍵字。
先實現一個簡單的例子,在暫停5秒後在控制檯輸出end
const sleep = (time) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve();
}, time);
})
};
const start = async () => {
console.log('start');
await sleep(5000);
console.log('end');
};
start();
在這裏 await 關鍵字必須修飾 Promise對象,且必須在async 修飾的函數內。這裏體現了外異步,內同步的思想。即start函數整體是異步的,但其函數內部的過程是順序進行的,即內同步。
基於上一節的問題,我們有一個文件列表,需要順次讀取其中的內容並打印。在這裏用 async 和 await 來實現
import fs from 'fs';
let fsList =['/a.txt','/b.txt','/c.txt','/d.txt','/e.txt'];
const readFile2 = (path) => {
return new Promise((resolve, reject) => {
fs.readFile(__dirname + path, 'utf-8', (err, data) => {
if(!err) resolve(data);
else reject(err);
})
})
}
const readFileList2 = async (fsList) => {
for(let i = 0; i < fsList.length; ++i){
console.log(await readFile2(fsList[i]);
}
console.log('Done...');
}
console.log('Start reading');
輸出:
Start reading
aaa
bbb
ccc
ddd
eee
Done...
看到結果,函數整體爲異步,因爲 Start reading 先輸出,
然後內部按照同步的方式依次輸出。
由於讀取的文件有大有小,我們不希望因爲一個文件很大而阻塞整體讀取的進程,因此如果不關心文件打印的順序,而希望其以最快的方式打印出來。假設有 3個文件 (a, b, c),讀取它們的時間分別爲 (ta, tb, tc),則同步讀取並打印的事件爲 ta + tb + tc, 而異步打印的事件則可接近於 max(ta, tb, tc)。
因此對於IO密集型服務,網絡請求數越大,則異步調用整體節省的更多,NodeJS異步回調的優勢就體現出來了。
那麼如何實現內部的併發調用呢?首先,如果要實現一個對數組的異步操作, Array.forEach和Array.map 方法都是不錯的選擇。下面的程序將數組 (值爲0到10000ms的隨機分佈) 作爲 setTimeOut等待時間的參數,依次打印出10個等待時間, 在打印結束後調用resolve輸出Done。
let t = [];
for(let i = 0; i < 10; ++i){
t.push(Math.random(i) * 10000);
}
let start = (callback) => {
return new Promise((resolve, reject) => {
let i = 0;
t.map((time, index, arr) => {
setTimeout(() => {
callback(`this is ${index + 1}s value of t, have waited ${time} ms`);
++i;
if(i >= 10) resolve('Done');
if(i >= 11) reject('an error occurs');
}, time);
});
})
}
start(time => console.log(time))
.then(t => console.log(t))
.catch(err => console.log(err));
console.log('Start counting');
輸出結果爲:
Start counting
858.5494470929777
1482.6545923241151
1699.61125447446
2207.0929478221115
2906.0818746170435
3986.756119433945
4298.932567413627
5385.720057993297
7303.359191124267
8855.286289748638
Done
由於其內部的異步方式,運行總時間爲 max(t[]) < 10s。
注意到程序終止(到達最大等待時間)時輸出Done的判斷條件,在內部 let i = 0
聲明一個計數值,每次+1,判斷到達最大值時退出。
Demo試驗成功後,迴歸到原問題,將該模板應用於readFile函數上,就能輕易實現內部的併發調用:
import fs from 'fs';
const readFileListAsync = (fsList, callback) => {
return new Promise((resolve, reject) => {
let i = 0;
fsList.map((file, index, fileList) => {
fs.readFile(__dirname + file, 'utf-8', (err, data) => {
++i;
if(err) reject(err);
if(data) callback(data);
if(i === fsList.length) {
resolve('Done');
return;
}
if(i > fsList.length) reject(new Error('an error occurs'));
})
})
})
}
readFileListAsync(fsList, data => console.log(data))
.then(data => console.log(data))
.catch(err => console.log(err));
console.log('Start reading');
輸出:
Start reading
aaa
ccc
bbb
eee
ddd
Done
由於其異步機制,每次輸出結果的順序將不固定。
到此我們以setTimeOut
及 readFile
函數爲例,實現了對數組元素循環進行異步操作的兩種方式,以 async & await 爲關鍵字實現的有序調用和以 Array.forEach 或 Array.map 實現的併發調用。
那麼是否存在更爲優雅的,甚至拋棄 Promise
以及 async&await
關鍵字來實現這兩種調用的方式呢,
在下一節中,筆者將採用then.js這個庫來實現。