NodeJS中有很多异步API,比如常见的fs模块的readFile方法。虽然有同步的版本readFileSync, 但是其性能肯定不如前者。所以这里从异步异步版本readFile说起:
const fs = require('fs');
fs.readFile('./a.txt', 'utf-8', function(error, data) {
if (!error) {
console.log('a.txt data:', data);
}
});
函数本身比较简单,三个参数分别是文件路径,数据编码和回调函数。
现在有这样一个需求,分别读取abc三个txt文件(文件内容分别是1, 2, 3,文件路径和js文件路径相同),按序输出文件内的内容,也就是输出1 2 3,如果并列读取,像这样:
const fs = require('fs');
fs.readFile('./a.txt', 'utf-8', function(error, data) {
if (!error) {
console.log('a.txt data:', data);
}
});
fs.readFile('./b.txt', 'utf-8', function(error, data) {
if (!error) {
console.log('b.txt data:', data);
}
});
fs.readFile('./c.txt', 'utf-8', function(error, data) {
if (!error) {
console.log('c.txt data:', data);
}
});
多次运行,会发现每次输出的顺序不一致:
当然不止这三种,有兴趣可以多运行几次看看。
所以要出现1 2 3固定输出,得这样写,读完a.txt才能读b,读完b才能读c:
const fs = require("fs");
fs.readFile("./a.txt", "utf-8", function (error, data) {
if (!error) {
console.log("a.txt data:", data);
fs.readFile("./b.txt", "utf-8", function (error, data) {
if (!error) {
console.log("b.txt data:", data);
fs.readFile("./c.txt", "utf-8", function (error, data) {
if (!error) {
console.log("c.txt data:", data);
}
});
}
});
}
});
这样虽然是按序输出了,但是代码嵌套了,如果有更多文件,且读取后的处理逻辑更加复杂,整个代码的可读性就变得很差。这个就是回调地狱。为了解决这个方法,我们引入了Promise。
Promise可以简单理解为包裹异步函数的容器,基本示例:
const fs = require("fs");
const readAPromise = new Promise(function (resolve, reject) {
fs.readFile("./a.txt", "utf-8", function (error, data) {
if (error) {
reject(error);
} else {
resolve(data);
}
});
});
readAPromise.then(function(result) {
console.log(result);
}).catch(function(error) {
console.error(error);
});
先试试链式调用来改造回调地狱代码:
const fs = require("fs");
function readFile(filePath, defaultCoding = "utf-8") {
return new Promise(function (resolve, reject) {
fs.readFile(filePath, defaultCoding, function (error, data) {
if (error) {
reject(error);
} else {
resolve(data);
}
});
});
}
readFile("./a.txt")
.then(function (data) {
console.log(data);
return readFile("./b.txt");
})
.then(function (data) {
console.log(data);
return readFile("./c.txt");
})
.then(function (data) {
console.log(data);
})
.catch(function (error) {
console.error(error);
});
然后用Promise.all试试:
const fs = require("fs");
function readFile(filePath, defaultCoding = "utf-8") {
return new Promise(function (resolve, reject) {
fs.readFile(filePath, defaultCoding, function (error, data) {
if (error) {
reject(error);
} else {
resolve(data);
}
});
});
}
Promise.all([readFile("./a.txt"), readFile("./b.txt"), readFile("./c.txt")])
.then(function (data) {
console.log(data);
})
.catch(function (error) {
console.error(error);
});
这里封装了一个返回文件读取结果Promise函数。然后调用Promise.all,第一个参数是个Promise对象数组。如果全部成功,就会到.then中的data去,data是各个promise resolve的结果数组,这里打印[ '1', '2', '3' ];如果有一个失败,整个Promise数组将走到.catch。要解决这个问题,可以尝试Promise.allSettled
const fs = require("fs");
function readFile(filePath, defaultCoding = "utf-8") {
return new Promise(function (resolve, reject) {
fs.readFile(filePath, defaultCoding, function (error, data) {
if (filePath === './b.txt') {
reject('cannot read b file');
}
if (error) {
reject(error);
} else {
resolve(data);
}
});
});
}
Promise.allSettled([readFile("./a.txt"), readFile("./b.txt"), readFile("./c.txt")])
.then(function (data) {
console.log(data);
})
.catch(function (error) {
console.error(error);
});
result:
[
{ status: 'fulfilled', value: '1' },
{ status: 'rejected', reason: 'cannot read b file' },
{ status: 'fulfilled', value: '3' }
]
还补充一点,使用async和await进一步优化:
异步函数基本用法:
async function f() {
return "a";
}
// console.log(f()); // Promise { 'a' }
f()
.then(function (data) {
console.log(data); // a
})
.catch(function (error) {
console.error(error);
});
完整示例
const fs = require("fs");
function readFile(filePath, defaultCoding = "utf-8") {
return new Promise(function (resolve, reject) {
fs.readFile(filePath, defaultCoding, function (error, data) {
if (error) {
reject(error);
} else {
resolve(data);
}
});
});
}
async function readAllFile() {
const aFile = await readFile("./a.txt");
const bFile = await readFile("./b.txt");
const cFile = await readFile("./c.txt");
return [aFile, bFile, cFile];
}
readAllFile().then(([a, b, c]) => console.log(a, b, c));
使用util模块的promisify方法再优化,可以去掉包装fs.readFile返回Promise函数,直接返回Promise对象而不用写回调,example:
const fs = require("fs");
const promisify = require('util').promisify;
const readFile = promisify(fs.readFile);
async function readAllFile() {
const aFile = await readFile("./a.txt", "utf-8");
const bFile = await readFile("./b.txt", "utf-8");
const cFile = await readFile("./c.txt", "utf-8");
return [aFile, bFile, cFile];
}
readAllFile().then(([a, b, c]) => console.log(a, b, c));