有哪些基于Node.js的前端面试题值得分享?
- 内容介绍
- 文章标签
- 相关推荐
本文共计4911个文字,预计阅读时间需要20分钟。
本章节为家长总结分享一些基于Node.js的前端面试题(附解析),希望对大家有所帮助!
一、Node基础概要
1.1 Node是什么?Node.js是一个开源与跨平台的JavaScript运行时环境。
1.2 Node.js是什么?Node.js是一个基于Chrome V8引擎的JavaScript运行时环境。
本篇文章给大家总结分享一些基于Node.js的前端面试题(附解析),希望对大家有所帮助!
一、Node基础概念1.1 Node是什么Node.js 是一个开源与跨平台的 JavaScript 运行时环境。在浏览器外运行 V8 JavaScript 引擎(Google Chrome 的内核),利用事件驱动、非阻塞和异步输入输出模型等技术提高性能。我们可以理解为:Node.js 就是一个服务器端的、非阻塞式I/O的、事件驱动的JavaScript运行环境。
理解Node,有几个基础的概念:非阻塞异步和事件驱动。
- 非阻塞异步: Nodejs采用了非阻塞型I/O机制,在做I/O操作的时候不会造成任何的阻塞,当完成之后,以时间的形式通知执行操作。例如,在执行了访问数据库的代码之后,将立即转而执行其后面的代码,把数据库返回结果的处理代码放在回调函数中,从而提高了程序的执行效率。
- 事件驱动: 事件驱动就是当进来一个新的请求的时,请求将会被压入一个事件队列中,然后通过一个循环来检测队列中的事件状态变化,如果检测到有状态变化的事件,那么就执行该事件对应的处理代码,一般都是回调函数。比如,读取一个文件,文件读取完毕后,就会触发对应的状态,然后通过对应的回调函数来进行处理。
Node.js适合用于I/O密集型应用,值的是应用在运行极限时,CPU占用率仍然比较低,大部分时间是在做 I/O硬盘内存读写操作。缺点如下:
- 不适合CPU密集型应用
- 只支持单核CPU,不能充分利用CPU
- 可靠性低,一旦代码某个环节崩溃,整个系统都崩溃
对于第三点,常用的解决方案是,使用Nnigx反向代理,开多个进程绑定多个端口,或者开多个进程监听同一个端口。
1.2.1 应用场景在熟悉了Nodejs的优点和弊端后,我们可以看到它适合以下的应用场景:
- 善于I/O,不善于计算。因为Nodejs是一个单线程,如果计算(同步)太多,则会阻塞这个线程。
- 大量并发的I/O,应用程序内部并不需要进行非常复杂的处理。
- 与 WeSocket 配合,开发长连接的实时交互应用程序。
具体的使用场景如下:
- 用户表单收集系统、后台管理系统、实时交互系统、考试系统、联网软件、高并发量的web应用程序。
- 基于web、canvas等多人联网游戏。
- 基于web的多人实时聊天客户端、聊天室、图文直播。
- 单页面浏览器应用程序。
- 操作数据库、为前端和移动端提供基于json的API。
在浏览器 JavaScript 中,window 是全局对象, 而 Nodejs 中的全局对象则是 global。
在NodeJS里,是不可能在最外层定义一个变量,因为所有的用户代码都是当前模块的,只在当前模块里可用,但可以通过exports对象的使用将其传递给模块外部。所以,在NodeJS中,用var声明的变量并不属于全局的变量,只在当前模块生效。像上述的global全局对象则在全局作用域中,任何全局变量、函数、对象都是该对象的一个属性值。
2.1 常见全局对象Node常见的全局对象有如下一些:
- Class:Buffer
- process
- console
- clearInterval、setInterval
- clearTimeout、setTimeout
- global
Class:BufferClass:Buffer可以用来处理二进制以及非Unicode编码的数据,在Buffer类实例化中存储了原始数据。Buffer类似于一个整数数组,在V8堆原始存储空间给它分配了内存,一旦创建了Buffer实例,则无法改变大小。
processprocess表示进程对象,提供有关当前过程的信息和控制。包括在执行node程序的过程中,如果需要传递参数,我们想要获取这个参数需要在process内置对象中。比如,我们有如下一个文件:
process.argv.forEach((val, index) => { console.log(`${index}: ${val}`); });
当我们需要启动一个进程时,可以使用下面的命令:
node index.js 参数...
consoleconsole主要用来打印stdout和stderr,最常用的比如日志输出:console.log。清空控制台的命令为:console.clear。如果需要打印函数的调用栈,可以使用命令console.trace。
clearInterval、setIntervalsetInterval用于设置定时器,语法格式如下:
setInterval(callback, delay[, ...args])
clearInterval则用于清除定时器,callback每delay毫秒重复执行一次。
clearTimeout、setTimeout
和setInterval一样,setTimeout主要用于设置延时器,而clearTimeout则用于清除设置的延时器。
globalglobal是一个全局命名空间对象,前面讲到的process、console、setTimeout等可以放到global中,例如:
console.log(process === global.process) //输出true2.2 模块中的全局对象
除了系统提供的全局对象外,还有一些只是在模块中出现,看起来像全局变量,如下所示:
- __dirname
- __filename
- exports
- module
- require
__dirname__dirname主要用于获取当前文件所在的路径,不包括后面的文件名。比如,在/Users/mjr 中运行 node example.js,打印结果如下:
console.log(__dirname); // 打印: /Users/mjr
__filename__filename用于获取当前文件所在的路径和文件名称,包括后面的文件名称。比如,在/Users/mjr 中运行 node example.js,打印的结果如下:
console.log(__filename);// 打印: /Users/mjr/example.js
exportsmodule.exports 用于导出一个指定模块所的内容,然后也可以使用require() 访问里面的内容。
exports.name = name;exports.age = age; exports.sayHello = sayHello;
requirerequire主要用于引入模块、 JSON、或本地文件, 可以从 node_modules 引入模块。可以使用相对路径引入本地模块或JSON文件,路径会根据__dirname定义的目录名或当前工作目录进行处理。
三、谈谈对process的理解3.1 基本概念我们知道,进程计算机系统进行资源分配和调度的基本单位,是操作系统结构的基础,是线程的容器。当我们启动一个js文件,实际就是开启了一个服务进程,每个进程都拥有自己的独立空间地址、数据栈,像另一个进程无法访问当前进程的变量、数据结构,只有数据通信后,进程之间才可以数据共享。
process 对象是Node的一个全局变量,提供了有关当前 Node.js 进程的信息并对其进行控制。 由于JavaScript是一个单线程语言,所以通过node xxx启动一个文件后,只有一条主线程。
3.2 常用属性和方法process的常见属性如下:
- process.env:环境变量,例如通过 `process.env.NODE_ENV 获取不同环境项目配置信息
- process.nextTick:这个在谈及 EventLoop 时经常为会提到
- process.pid:获取当前进程id
- process.ppid:当前进程对应的父进程
- process.cwd():获取当前进程工作目录
- process.platform:获取当前进程运行的操作系统平台
- process.uptime():当前进程已运行时间,例如:pm2 守护进程的 uptime 值
进程事件: process.on(‘uncaughtException’,cb) 捕获异常信息、 process.on(‘exit’,cb)进程推出监听
- 三个标准流: process.stdout 标准输出、 process.stdin 标准输入、 process.stderr 标准错误输出
- process.title:用于指定进程名称,有的时候需要给进程指定一个名称
fs(filesystem)是文件系统模块,该模块提供本地文件的读写能力,基本上是POSIX文件操作命令的简单包装。可以说,所有与文件的操作都是通过fs核心模块来实现的。
使用之前,需要先导入fs模块,如下:
const fs = require('fs');4.2 文件基础知识
在计算机中,有关于文件的基础知识有如下一些:
- 权限位 mode
- 标识位 flag
- 文件描述为 fd
针对文件所有者、文件所属组、其他用户进行权限分配,其中类型又分成读、写和执行,具备权限位4、2、1,不具备权限为0。如在linux查看文件权限位的命令如下:
drwxr-xr-x 1 PandaShen 197121 0 Jun 28 14:41 core -rw-r--r-- 1 PandaShen 197121 293 Jun 23 17:44 index.md
在开头前十位中,d为文件夹,-为文件,后九位就代表当前用户、用户所属组和其他用户的权限位,按每三位划分,分别代表读(r)、写(w)和执行(x),- 代表没有当前位对应的权限。
4.2.2 标识位标识位代表着对文件的操作方式,如可读、可写、即可读又可写等等,如下表所示:
4.2.3 文件描述 fd操作系统会为每个打开的文件分配一个名为文件描述符的数值标识,文件操作使用这些文件描述符来识别与追踪每个特定的文件。
Window 系统使用了一个不同但概念类似的机制来追踪资源,为方便用户,NodeJS 抽象了不同操作系统间的差异,为所有打开的文件分配了数值的文件描述符。
在 NodeJS 中,每操作一个文件,文件描述符是递增的,文件描述符一般从 3 开始,因为前面有 0、1、2三个比较特殊的描述符,分别代表 process.stdin(标准输入)、process.stdout(标准输出)和 process.stderr(错误输出)。
4.3 常用方法由于fs模块主要是操作文件的,所以常见的文件操作方法有如下一些:
- 文件读取
- 文件写入
- 文件追加写入
- 文件拷贝
- 创建目录
常用的文件读取有readFileSync和readFile两个方法。其中,readFileSync表示同步读取,如下:
const fs = require("fs"); let buf = fs.readFileSync("1.txt"); let data = fs.readFileSync("1.txt", "utf8"); console.log(buf); // <Buffer 48 65 6c 6c 6f> console.log(data); // Hello
- 第一个参数为读取文件的路径或文件描述符。
- 第二个参数为 options,默认值为 null,其中有 encoding(编码,默认为 null)和 flag(标识位,默认为 r),也可直接传入 encoding。
readFile为异步读取方法, readFile 与 readFileSync 的前两个参数相同,最后一个参数为回调函数,函数内有两个参数 err(错误)和 data(数据),该方法没有返回值,回调函数在读取文件成功后执行。
const fs = require("fs"); fs.readFile("1.txt", "utf8", (err, data) => { if(!err){ console.log(data); // Hello } });4.3.2 文件写入
文件写入需要用到writeFileSync和writeFile两个方法。writeFileSync表示同步写入,如下所示。
const fs = require("fs"); fs.writeFileSync("2.txt", "Hello world"); let data = fs.readFileSync("2.txt", "utf8"); console.log(data); // Hello world
- 第一个参数为写入文件的路径或文件描述符。
- 第二个参数为写入的数据,类型为 String 或 Buffer。
- 第三个参数为 options,默认值为 null,其中有 encoding(编码,默认为 utf8)、 flag(标识位,默认为 w)和 mode(权限位,默认为 0o666),也可直接传入 encoding。
writeFile表示异步写入,writeFile 与 writeFileSync 的前三个参数相同,最后一个参数为回调函数,函数内有一个参数 err(错误),回调函数在文件写入数据成功后执行。
const fs = require("fs"); fs.writeFile("2.txt", "Hello world", err => { if (!err) { fs.readFile("2.txt", "utf8", (err, data) => { console.log(data); // Hello world }); } });4.3.3 文件追加写入
文件追加写入需要用到appendFileSync和appendFile两个方法。appendFileSync表示同步写入,如下。
const fs = require("fs"); fs.appendFileSync("3.txt", " world"); let data = fs.readFileSync("3.txt", "utf8");
- 第一个参数为写入文件的路径或文件描述符。
- 第二个参数为写入的数据,类型为 String 或 Buffer。
- 第三个参数为 options,默认值为 null,其中有 encoding(编码,默认为 utf8)、 flag(标识位,默认为 a)和 mode(权限位,默认为 0o666),也可直接传入 encoding。
appendFile表示异步追加写入,方法 appendFile 与 appendFileSync 的前三个参数相同,最后一个参数为回调函数,函数内有一个参数 err(错误),回调函数在文件追加写入数据成功后执行,如下所示。
const fs = require("fs"); fs.appendFile("3.txt", " world", err => { if (!err) { fs.readFile("3.txt", "utf8", (err, data) => { console.log(data); // Hello world }); } });4.3.4 创建目录
创建目录主要有mkdirSync和mkdir两个方法。其中,mkdirSync为同步创建,参数为一个目录的路径,没有返回值,在创建目录的过程中,必须保证传入的路径前面的文件目录都存在,否则会抛出异常。
// 假设已经有了 a 文件夹和 a 下的 b 文件夹 fs.mkdirSync("a/b/c")
mkdir为异步创建,第二个参数为回调函数,如下所示。
fs.mkdir("a/b/c", err => { if (!err) console.log("创建成功"); });五、谈谈你对Stream 的理解5.1 基本概念
流(Stream)是一种数据传输的手段,是一种端到端信息交换的方式,而且是有顺序的,是逐块读取数据、处理内容,用于顺序读取输入或写入输出。在Node中,Stream分成三部分:source、dest、pipe。
其中,在source和dest之间有一个连接的管道pipe,它的基本语法是source.pipe(dest),source和dest就是通过pipe连接,让数据从source流向dest,如下图所示:
5.2 流的分类在Node,流可以分成四个种类:
- 可写流:可写入数据的流,例如 fs.createWriteStream() 可以使用流将数据写入文件。
- 可读流: 可读取数据的流,例如fs.createReadStream() 可以从文件读取内容。
- 双工流: 既可读又可写的流,例如 net.Socket。
- 转换流: 可以在数据写入和读取时修改或转换数据的流。例如,在文件压缩操作中,可以向文件写入压缩数据,并从文件中读取解压数据。
在Node的HTTP服务器模块中,request 是可读流,response 是可写流。对于fs 模块来说,能同时处理可读和可写文件流可读流和可写流都是单向的,比较容易理解。而Socket是双向的,可读可写。
5.2.1 双工流在Node中,比较的常见的全双工通信就是websocket,因为发送方和接受方都是各自独立的方法,发送和接收都没有任何关系。
基本的使用方法如下:
const { Duplex } = require('stream'); const myDuplex = new Duplex({ read(size) { // ... }, write(chunk, encoding, callback) { // ... } });5.3 使用场景
流的常见使用场景有:
- get请求返回文件给客户端
- 文件操作
- 一些打包工具的底层操作
流一个常见的使用场景就是网络请求,比如使用stream流返回文件,res也是一个stream对象,通过pipe管道将文件数据返回。
const server = localhost:12333 ,即可看到进程界面,更详细的内容请参考官网
10.3 Node性能优化关于Node的性能优化的方式有如下几个:
- 使用最新版本Node.js
- 正确使用流 Stream
- 代码层面优化
- 内存管理优化
每个版本的性能提升主要来自于两个方面:
- V8 的版本更新
- Node.js 内部代码的更新优化
在Node中,很多对象都实现了流,对于一个大文件可以通过流的形式发送,不需要将其完全读入内存。
const http = require('http'); const fs = require('fs'); // 错误方式 http.createServer(function (req, res) { fs.readFile(__dirname + '/data.txt', function (err, data) { res.end(data); }); }); // 正确方式 http.createServer(function (req, res) { const stream = fs.createReadStream(__dirname + '/data.txt'); stream.pipe(res); });10.3.3 代码层面优化
合并查询,将多次查询合并一次,减少数据库的查询次数。
// 错误方式 for user_id in userIds let account = user_account.findOne(user_id) // 正确方式 const user_account_map = {} // 注意这个对象将会消耗大量内存。 user_account.find(user_id in user_ids).forEach(account){ user_account_map[account.user_id] = account } for user_id in userIds var account = user_account_map[user_id]10.3.4 内存管理优化
在 V8 中,主要将内存分为新生代和老生代两代:
- 新生代:对象的存活时间较短。新生对象或只经过一次垃圾回收的对象。
- 老生代:对象存活时间较长。经历过一次或多次垃圾回收的对象。
若新生代内存空间不够,直接分配到老生代。通过减少内存占用,可以提高服务器的性能。如果有内存泄露,也会导致大量的对象存储到老生代中,服务器性能会大大降低,比如下面的例子。
const buffer = fs.readFileSync(__dirname + '/source/index.htm'); app.use( mount('/', async (ctx) => { ctx.status = 200; ctx.type = 'html'; ctx.body = buffer; leak.push(fs.readFileSync(__dirname + '/source/index.htm')); }) ); const leak = [];
当leak的内存非常大的时候,就有可能造成内存泄露,应当避免这样的操作。
减少内存使用,可以明显的提高服务性能。而节省内存最好的方式是使用池,其将频用、可复用对象存储起来,减少创建和销毁操作。例如有个图片请求接口,每次请求,都需要用到类。若每次都需要重新new这些类,并不是很合适,在大量请求时,频繁创建和销毁这些类,造成内存抖动。而使用对象池的机制,对这种频繁需要创建和销毁的对象保存在一个对象池中,从而避免重读的初始化操作,从而提高框架的性能。
更多编程相关知识,请访问:编程视频!!
本文共计4911个文字,预计阅读时间需要20分钟。
本章节为家长总结分享一些基于Node.js的前端面试题(附解析),希望对大家有所帮助!
一、Node基础概要
1.1 Node是什么?Node.js是一个开源与跨平台的JavaScript运行时环境。
1.2 Node.js是什么?Node.js是一个基于Chrome V8引擎的JavaScript运行时环境。
本篇文章给大家总结分享一些基于Node.js的前端面试题(附解析),希望对大家有所帮助!
一、Node基础概念1.1 Node是什么Node.js 是一个开源与跨平台的 JavaScript 运行时环境。在浏览器外运行 V8 JavaScript 引擎(Google Chrome 的内核),利用事件驱动、非阻塞和异步输入输出模型等技术提高性能。我们可以理解为:Node.js 就是一个服务器端的、非阻塞式I/O的、事件驱动的JavaScript运行环境。
理解Node,有几个基础的概念:非阻塞异步和事件驱动。
- 非阻塞异步: Nodejs采用了非阻塞型I/O机制,在做I/O操作的时候不会造成任何的阻塞,当完成之后,以时间的形式通知执行操作。例如,在执行了访问数据库的代码之后,将立即转而执行其后面的代码,把数据库返回结果的处理代码放在回调函数中,从而提高了程序的执行效率。
- 事件驱动: 事件驱动就是当进来一个新的请求的时,请求将会被压入一个事件队列中,然后通过一个循环来检测队列中的事件状态变化,如果检测到有状态变化的事件,那么就执行该事件对应的处理代码,一般都是回调函数。比如,读取一个文件,文件读取完毕后,就会触发对应的状态,然后通过对应的回调函数来进行处理。
Node.js适合用于I/O密集型应用,值的是应用在运行极限时,CPU占用率仍然比较低,大部分时间是在做 I/O硬盘内存读写操作。缺点如下:
- 不适合CPU密集型应用
- 只支持单核CPU,不能充分利用CPU
- 可靠性低,一旦代码某个环节崩溃,整个系统都崩溃
对于第三点,常用的解决方案是,使用Nnigx反向代理,开多个进程绑定多个端口,或者开多个进程监听同一个端口。
1.2.1 应用场景在熟悉了Nodejs的优点和弊端后,我们可以看到它适合以下的应用场景:
- 善于I/O,不善于计算。因为Nodejs是一个单线程,如果计算(同步)太多,则会阻塞这个线程。
- 大量并发的I/O,应用程序内部并不需要进行非常复杂的处理。
- 与 WeSocket 配合,开发长连接的实时交互应用程序。
具体的使用场景如下:
- 用户表单收集系统、后台管理系统、实时交互系统、考试系统、联网软件、高并发量的web应用程序。
- 基于web、canvas等多人联网游戏。
- 基于web的多人实时聊天客户端、聊天室、图文直播。
- 单页面浏览器应用程序。
- 操作数据库、为前端和移动端提供基于json的API。
在浏览器 JavaScript 中,window 是全局对象, 而 Nodejs 中的全局对象则是 global。
在NodeJS里,是不可能在最外层定义一个变量,因为所有的用户代码都是当前模块的,只在当前模块里可用,但可以通过exports对象的使用将其传递给模块外部。所以,在NodeJS中,用var声明的变量并不属于全局的变量,只在当前模块生效。像上述的global全局对象则在全局作用域中,任何全局变量、函数、对象都是该对象的一个属性值。
2.1 常见全局对象Node常见的全局对象有如下一些:
- Class:Buffer
- process
- console
- clearInterval、setInterval
- clearTimeout、setTimeout
- global
Class:BufferClass:Buffer可以用来处理二进制以及非Unicode编码的数据,在Buffer类实例化中存储了原始数据。Buffer类似于一个整数数组,在V8堆原始存储空间给它分配了内存,一旦创建了Buffer实例,则无法改变大小。
processprocess表示进程对象,提供有关当前过程的信息和控制。包括在执行node程序的过程中,如果需要传递参数,我们想要获取这个参数需要在process内置对象中。比如,我们有如下一个文件:
process.argv.forEach((val, index) => { console.log(`${index}: ${val}`); });
当我们需要启动一个进程时,可以使用下面的命令:
node index.js 参数...
consoleconsole主要用来打印stdout和stderr,最常用的比如日志输出:console.log。清空控制台的命令为:console.clear。如果需要打印函数的调用栈,可以使用命令console.trace。
clearInterval、setIntervalsetInterval用于设置定时器,语法格式如下:
setInterval(callback, delay[, ...args])
clearInterval则用于清除定时器,callback每delay毫秒重复执行一次。
clearTimeout、setTimeout
和setInterval一样,setTimeout主要用于设置延时器,而clearTimeout则用于清除设置的延时器。
globalglobal是一个全局命名空间对象,前面讲到的process、console、setTimeout等可以放到global中,例如:
console.log(process === global.process) //输出true2.2 模块中的全局对象
除了系统提供的全局对象外,还有一些只是在模块中出现,看起来像全局变量,如下所示:
- __dirname
- __filename
- exports
- module
- require
__dirname__dirname主要用于获取当前文件所在的路径,不包括后面的文件名。比如,在/Users/mjr 中运行 node example.js,打印结果如下:
console.log(__dirname); // 打印: /Users/mjr
__filename__filename用于获取当前文件所在的路径和文件名称,包括后面的文件名称。比如,在/Users/mjr 中运行 node example.js,打印的结果如下:
console.log(__filename);// 打印: /Users/mjr/example.js
exportsmodule.exports 用于导出一个指定模块所的内容,然后也可以使用require() 访问里面的内容。
exports.name = name;exports.age = age; exports.sayHello = sayHello;
requirerequire主要用于引入模块、 JSON、或本地文件, 可以从 node_modules 引入模块。可以使用相对路径引入本地模块或JSON文件,路径会根据__dirname定义的目录名或当前工作目录进行处理。
三、谈谈对process的理解3.1 基本概念我们知道,进程计算机系统进行资源分配和调度的基本单位,是操作系统结构的基础,是线程的容器。当我们启动一个js文件,实际就是开启了一个服务进程,每个进程都拥有自己的独立空间地址、数据栈,像另一个进程无法访问当前进程的变量、数据结构,只有数据通信后,进程之间才可以数据共享。
process 对象是Node的一个全局变量,提供了有关当前 Node.js 进程的信息并对其进行控制。 由于JavaScript是一个单线程语言,所以通过node xxx启动一个文件后,只有一条主线程。
3.2 常用属性和方法process的常见属性如下:
- process.env:环境变量,例如通过 `process.env.NODE_ENV 获取不同环境项目配置信息
- process.nextTick:这个在谈及 EventLoop 时经常为会提到
- process.pid:获取当前进程id
- process.ppid:当前进程对应的父进程
- process.cwd():获取当前进程工作目录
- process.platform:获取当前进程运行的操作系统平台
- process.uptime():当前进程已运行时间,例如:pm2 守护进程的 uptime 值
进程事件: process.on(‘uncaughtException’,cb) 捕获异常信息、 process.on(‘exit’,cb)进程推出监听
- 三个标准流: process.stdout 标准输出、 process.stdin 标准输入、 process.stderr 标准错误输出
- process.title:用于指定进程名称,有的时候需要给进程指定一个名称
fs(filesystem)是文件系统模块,该模块提供本地文件的读写能力,基本上是POSIX文件操作命令的简单包装。可以说,所有与文件的操作都是通过fs核心模块来实现的。
使用之前,需要先导入fs模块,如下:
const fs = require('fs');4.2 文件基础知识
在计算机中,有关于文件的基础知识有如下一些:
- 权限位 mode
- 标识位 flag
- 文件描述为 fd
针对文件所有者、文件所属组、其他用户进行权限分配,其中类型又分成读、写和执行,具备权限位4、2、1,不具备权限为0。如在linux查看文件权限位的命令如下:
drwxr-xr-x 1 PandaShen 197121 0 Jun 28 14:41 core -rw-r--r-- 1 PandaShen 197121 293 Jun 23 17:44 index.md
在开头前十位中,d为文件夹,-为文件,后九位就代表当前用户、用户所属组和其他用户的权限位,按每三位划分,分别代表读(r)、写(w)和执行(x),- 代表没有当前位对应的权限。
4.2.2 标识位标识位代表着对文件的操作方式,如可读、可写、即可读又可写等等,如下表所示:
4.2.3 文件描述 fd操作系统会为每个打开的文件分配一个名为文件描述符的数值标识,文件操作使用这些文件描述符来识别与追踪每个特定的文件。
Window 系统使用了一个不同但概念类似的机制来追踪资源,为方便用户,NodeJS 抽象了不同操作系统间的差异,为所有打开的文件分配了数值的文件描述符。
在 NodeJS 中,每操作一个文件,文件描述符是递增的,文件描述符一般从 3 开始,因为前面有 0、1、2三个比较特殊的描述符,分别代表 process.stdin(标准输入)、process.stdout(标准输出)和 process.stderr(错误输出)。
4.3 常用方法由于fs模块主要是操作文件的,所以常见的文件操作方法有如下一些:
- 文件读取
- 文件写入
- 文件追加写入
- 文件拷贝
- 创建目录
常用的文件读取有readFileSync和readFile两个方法。其中,readFileSync表示同步读取,如下:
const fs = require("fs"); let buf = fs.readFileSync("1.txt"); let data = fs.readFileSync("1.txt", "utf8"); console.log(buf); // <Buffer 48 65 6c 6c 6f> console.log(data); // Hello
- 第一个参数为读取文件的路径或文件描述符。
- 第二个参数为 options,默认值为 null,其中有 encoding(编码,默认为 null)和 flag(标识位,默认为 r),也可直接传入 encoding。
readFile为异步读取方法, readFile 与 readFileSync 的前两个参数相同,最后一个参数为回调函数,函数内有两个参数 err(错误)和 data(数据),该方法没有返回值,回调函数在读取文件成功后执行。
const fs = require("fs"); fs.readFile("1.txt", "utf8", (err, data) => { if(!err){ console.log(data); // Hello } });4.3.2 文件写入
文件写入需要用到writeFileSync和writeFile两个方法。writeFileSync表示同步写入,如下所示。
const fs = require("fs"); fs.writeFileSync("2.txt", "Hello world"); let data = fs.readFileSync("2.txt", "utf8"); console.log(data); // Hello world
- 第一个参数为写入文件的路径或文件描述符。
- 第二个参数为写入的数据,类型为 String 或 Buffer。
- 第三个参数为 options,默认值为 null,其中有 encoding(编码,默认为 utf8)、 flag(标识位,默认为 w)和 mode(权限位,默认为 0o666),也可直接传入 encoding。
writeFile表示异步写入,writeFile 与 writeFileSync 的前三个参数相同,最后一个参数为回调函数,函数内有一个参数 err(错误),回调函数在文件写入数据成功后执行。
const fs = require("fs"); fs.writeFile("2.txt", "Hello world", err => { if (!err) { fs.readFile("2.txt", "utf8", (err, data) => { console.log(data); // Hello world }); } });4.3.3 文件追加写入
文件追加写入需要用到appendFileSync和appendFile两个方法。appendFileSync表示同步写入,如下。
const fs = require("fs"); fs.appendFileSync("3.txt", " world"); let data = fs.readFileSync("3.txt", "utf8");
- 第一个参数为写入文件的路径或文件描述符。
- 第二个参数为写入的数据,类型为 String 或 Buffer。
- 第三个参数为 options,默认值为 null,其中有 encoding(编码,默认为 utf8)、 flag(标识位,默认为 a)和 mode(权限位,默认为 0o666),也可直接传入 encoding。
appendFile表示异步追加写入,方法 appendFile 与 appendFileSync 的前三个参数相同,最后一个参数为回调函数,函数内有一个参数 err(错误),回调函数在文件追加写入数据成功后执行,如下所示。
const fs = require("fs"); fs.appendFile("3.txt", " world", err => { if (!err) { fs.readFile("3.txt", "utf8", (err, data) => { console.log(data); // Hello world }); } });4.3.4 创建目录
创建目录主要有mkdirSync和mkdir两个方法。其中,mkdirSync为同步创建,参数为一个目录的路径,没有返回值,在创建目录的过程中,必须保证传入的路径前面的文件目录都存在,否则会抛出异常。
// 假设已经有了 a 文件夹和 a 下的 b 文件夹 fs.mkdirSync("a/b/c")
mkdir为异步创建,第二个参数为回调函数,如下所示。
fs.mkdir("a/b/c", err => { if (!err) console.log("创建成功"); });五、谈谈你对Stream 的理解5.1 基本概念
流(Stream)是一种数据传输的手段,是一种端到端信息交换的方式,而且是有顺序的,是逐块读取数据、处理内容,用于顺序读取输入或写入输出。在Node中,Stream分成三部分:source、dest、pipe。
其中,在source和dest之间有一个连接的管道pipe,它的基本语法是source.pipe(dest),source和dest就是通过pipe连接,让数据从source流向dest,如下图所示:
5.2 流的分类在Node,流可以分成四个种类:
- 可写流:可写入数据的流,例如 fs.createWriteStream() 可以使用流将数据写入文件。
- 可读流: 可读取数据的流,例如fs.createReadStream() 可以从文件读取内容。
- 双工流: 既可读又可写的流,例如 net.Socket。
- 转换流: 可以在数据写入和读取时修改或转换数据的流。例如,在文件压缩操作中,可以向文件写入压缩数据,并从文件中读取解压数据。
在Node的HTTP服务器模块中,request 是可读流,response 是可写流。对于fs 模块来说,能同时处理可读和可写文件流可读流和可写流都是单向的,比较容易理解。而Socket是双向的,可读可写。
5.2.1 双工流在Node中,比较的常见的全双工通信就是websocket,因为发送方和接受方都是各自独立的方法,发送和接收都没有任何关系。
基本的使用方法如下:
const { Duplex } = require('stream'); const myDuplex = new Duplex({ read(size) { // ... }, write(chunk, encoding, callback) { // ... } });5.3 使用场景
流的常见使用场景有:
- get请求返回文件给客户端
- 文件操作
- 一些打包工具的底层操作
流一个常见的使用场景就是网络请求,比如使用stream流返回文件,res也是一个stream对象,通过pipe管道将文件数据返回。
const server = localhost:12333 ,即可看到进程界面,更详细的内容请参考官网
10.3 Node性能优化关于Node的性能优化的方式有如下几个:
- 使用最新版本Node.js
- 正确使用流 Stream
- 代码层面优化
- 内存管理优化
每个版本的性能提升主要来自于两个方面:
- V8 的版本更新
- Node.js 内部代码的更新优化
在Node中,很多对象都实现了流,对于一个大文件可以通过流的形式发送,不需要将其完全读入内存。
const http = require('http'); const fs = require('fs'); // 错误方式 http.createServer(function (req, res) { fs.readFile(__dirname + '/data.txt', function (err, data) { res.end(data); }); }); // 正确方式 http.createServer(function (req, res) { const stream = fs.createReadStream(__dirname + '/data.txt'); stream.pipe(res); });10.3.3 代码层面优化
合并查询,将多次查询合并一次,减少数据库的查询次数。
// 错误方式 for user_id in userIds let account = user_account.findOne(user_id) // 正确方式 const user_account_map = {} // 注意这个对象将会消耗大量内存。 user_account.find(user_id in user_ids).forEach(account){ user_account_map[account.user_id] = account } for user_id in userIds var account = user_account_map[user_id]10.3.4 内存管理优化
在 V8 中,主要将内存分为新生代和老生代两代:
- 新生代:对象的存活时间较短。新生对象或只经过一次垃圾回收的对象。
- 老生代:对象存活时间较长。经历过一次或多次垃圾回收的对象。
若新生代内存空间不够,直接分配到老生代。通过减少内存占用,可以提高服务器的性能。如果有内存泄露,也会导致大量的对象存储到老生代中,服务器性能会大大降低,比如下面的例子。
const buffer = fs.readFileSync(__dirname + '/source/index.htm'); app.use( mount('/', async (ctx) => { ctx.status = 200; ctx.type = 'html'; ctx.body = buffer; leak.push(fs.readFileSync(__dirname + '/source/index.htm')); }) ); const leak = [];
当leak的内存非常大的时候,就有可能造成内存泄露,应当避免这样的操作。
减少内存使用,可以明显的提高服务性能。而节省内存最好的方式是使用池,其将频用、可复用对象存储起来,减少创建和销毁操作。例如有个图片请求接口,每次请求,都需要用到类。若每次都需要重新new这些类,并不是很合适,在大量请求时,频繁创建和销毁这些类,造成内存抖动。而使用对象池的机制,对这种频繁需要创建和销毁的对象保存在一个对象池中,从而避免重读的初始化操作,从而提高框架的性能。
更多编程相关知识,请访问:编程视频!!

