第 47 章 fs 模块

Node.js 是一个异步事件驱动的 JavaScript 运行环境,读写文件有同步和异步区别。在使用fs模块之前,需要有回调函数、promise对象的基础知识。

47.1 回调函数

在 JavaScript 中,回调函数具体的定义为:函数 A 作为参数(函数引用)传递到另一个函数 B 中,并且函数 B 运行完成后再执行函数 A。我们就把函数 A 叫做回调函数。例如:

function doSomething(msg, callback) {
    alert(msg);
    if (typeof callback == "function")
    callback();
};

doSomething("存 5000 块", function () {
    alert("稍等,马上办理");
    alert('2 分钟后,您的业务已办理完毕');
});

回调与同步、异步并没有直接的联系,回调只是一种实现方式,既可以有同步回调,也可以有异步回调。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用,用于对该事件或条件进行响应。

47.2 同步与异步的区别

下面这段代码的执行结果,就能看出不同区别:

function doSomething(msg, callback) {
    console.log(msg);
    if (typeof callback == "function")
        callback();
};

doSomething("存 5000 块", function () {
    console.log("稍等,马上办理");
    setTimeout(function () { console.log('2 分钟后,您的业务已办理完毕'); }, 2000);
    // console.log('2 分钟后,您的业务已办理完毕');
});

setTimeout(function () { console.log('5'); }, 5000);
console.log('1');
function test() {
    setTimeout(function () { console.log('2'); }, 1000);
}
test();

console.log('3');
setTimeout(function () { console.log('4'); }, 2000);

与过程式编程相比,异步对变成带来的一些变化:因为有很多情况下是代码要求先前的代码执行完毕,如要调用之前处理的数据结果、和数据库交互等。Node.js 中可以采用回调方式解决这个问题。

47.3 使用fs模块读取文件

有同步、异步和promise对象三种方式,推荐使用promise对象。

先看一个案例:

var fs = require('fs');
fs.readFile('./files/1.txt', 'utf8', function (err, data) {
    if (err) {
        throw err;
    }
    console.log(data);
});

fs.readFile('./files/2.txt', 'utf8', function (err, data) {
    if (err) {
        throw err;
    }
    console.log(data);
});

fs.readFile('./files/3.txt', 'utf8', function (err, data) {
    if (err) {
        throw err;
    }
    console.log(data);
});

上述代码并不能保证按照1、2、3的顺序输出结果,会按照实际读取的结果展示。因为他们都是异步操作,不知道谁先读取完。 这得取决于异步任务IO的耗时。

为了确保按照顺序执行,一开始,人们的解决方案是在回调函数中,使用回调函数。代码如下:

fs.readFile('./files/1.txt', 'utf8', function (err, data) {
    if (err) {
        throw err;
    } else {
        console.log(data);
        fs.readFile('./files/2.txt', 'utf8', function (err, data) {
            if (err) {
                throw err;
            } else {
                console.log(data);
                fs.readFile('./files/3.txt', 'utf8', function (err, data) {
                    if (err) {
                        throw err;
                    }
                    console.log(data);
                });
            }
        });
    }
});

以上按照顺序执行多个异步任务产生的问题:回调地狱问题(层层包裹进行回调,代码也不够优雅)

在ES6中,提供了promise对象来解决上述问题。https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Promise

47.4 promise对象

Promise 对象是由关键字 new 及其构造函数来创建的。该构造函数会把一个叫做“处理器函数”(executor function)的函数作为它的参数。这个“处理器函数”接受两个函数——resolvereject ——作为其参数。当异步任务顺利完成且返回结果值时,会调用 resolve 函数;而当异步任务失败且返回失败原因(通常是一个错误对象)时,会调用reject 函数。

new Promise返回的是一个promise对象,这个对象有一个方法叫做then,在其原型对象上通过这then方法可以指定成功和失败的回调函数。

语法:promise.then(successCallback,errorCallback);

promise.then(function (data) {
  //then第一个函数是成功的回调,参数是resolve(err)中的data
  console.log('成功:' + data); // 若成功,运行结果:成功:111
}).catch(function(err){
  //then第二参数错误回调换成这里catch也行,两者选其一
  console.log('err');
}).finally(function(){
  //无论失败成功都会执行
  console.log('完成');
})

例如,可以自定义promise方式读取文件:

function readFilePromise(path, encoding = "utf8") {
    const promise = new Promise((resolve, reject) => {
        fs.readFile(path, (err, data) => {
            if (err) return reject(err);
            return resolve(data.toString(encoding));
        });
    });
    return promise;
}
 
readFilePromise("./package.json").then(res => console.log(res));

在 Node.js v12 中,引入了 fs Promise api。它们返回 Promise 对象而不是使用回调。 API 可通过 require('fs').promises 访问。

const fsPromises = require("fs").promises;
fsPromises.readFile("./package.json", {encoding: "utf8", flag: "r"})
    .then(console.log)
    .catch(console.error);