NodeJS 異步操作:事件隊列的有序與併發操作

在上一節中我們介紹了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

由於其異步機制,每次輸出結果的順序將不固定。

到此我們以setTimeOutreadFile 函數爲例,實現了對數組元素循環進行異步操作的兩種方式,以 async & await 爲關鍵字實現的有序調用和以 Array.forEach 或 Array.map 實現的併發調用。

那麼是否存在更爲優雅的,甚至拋棄 Promise 以及 async&await 關鍵字來實現這兩種調用的方式呢,
在下一節中,筆者將採用then.js這個庫來實現。

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