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这个库来实现。

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