Review
- 2023-02-15 22:47
一、Introduction #
Node 中的 readFile API 工作机制 #
var fs = require('fs')
// 同步
var data = fs.readFileSync('test.js')
function fileHanlder(err, data){
data.toString()
}
// 异步
fs.readFile('test.txt', fileHanlder)Node体系架构 ![[e0ce0a3be7bd_6e85bdc3.webp]]
Node 是 V8 的宿主,它会给 V8 提供事件循环和消息队列。在 Node 中,事件循环是由 libuv 提供的,libuv 工作在主线程中,它会从消息队列中取出事件,并在主线程上执行事件。
同样,对于一些主线程上不适合处理的事件,比如消耗时间过久的网络资源下载、文件读写、设备访问等,Node 会提供很多线程来处理这些事件,把这些线程称为线程池。
通常,在 Node 中认为读写文件是一个非常耗时的工作,因此主线程会将回调函数和读文件的操作一道发送给文件读写线程,并让实际的读写操作运行在读写线程中。
比如当在 Node 的主线程上执行 readFile 的时候,主线程会将 readFile 的文件名称和回调函数,提交给文件读写线程来处理,具体过程如下所示:

文件读写线程完成了文件读取之后,会将结果和回调函数封装成新的事件,并将其添加进消息队列中。比如文件线程将读取的文件内容存放在内存中,并将 data 指针指向了该内存,然后文件读写线程会将 data 和回调函数封装成新的事件,并将其丢进消息队列中,具体过程如下所示:

等到 libuv 从消息队列中读取该事件后,主线程就可以着手来处理该事件了。在主线程处理该事件的过程中,主线程调用事件中的回调函数,并将 data 结果数据作为参数,如下图所示:

然后在回调函数中,我们就可以拿到读取的结果来实现一些业务逻辑了。
不过,总有些人觉得异步读写文件操作过于复杂了,如果读取的文件体积不大或者项目瓶颈不在文件读写,那么依然使用异步调用和回调函数的模式就显得有点过度复杂了。
因此 Node 还提供了一套同步读写的 API。第一段代码中的 readFileSync 就是同步实现的,同步代码非常简单,当 libuv 读取到 readFileSync 的任务后,就直接在主线程上执行读写操作,等待读写结束,直接返回读写的结果,这也是同步回调的一种应用。当然在读写过程中,消息队列中的其他任务是无法被执行的。
所以在选择使用同步 API 还是异步 API 时,要看实际的场景,并不是非 A 即 B。
几种内存问题 #
内存问题至关重要,因为通过内存而造成的问题很容易被用户察觉。总的来说,内存问题可以定义为下面这三类:
- 内存泄漏 (Memory leak),它会导致页面的性能越来越差;
- 内存膨胀 (Memory bloat),它会导致页面的性能会一直很差;
- 频繁垃圾回收,它会导致页面出现延迟或者经常暂停。
内存泄漏 #
本质上,内存泄漏可以定义为:当进程不再需要某些内存的时候,这些不再被需要的内存依然没有被进程回收。
在 JavaScript 中,造成内存泄漏 (Memory leak) 的主要原因是不再需要 (没有作用) 的内存数据依然被其他对象引用着。
举例
function foo() {
//创建一个临时的temp_array
temp_array = new Array(200000)
/**
* 使用temp_array
*/
}当执行这段代码时,由于函数体内的对象没有被 var、let、const 这些关键字声明,那么 V8 就会使用 this.temp_array 替换 temp_array。
function foo() {
//创建一个临时的temp_array
this.temp_array = new Array(200000)
/**
* this.temp_array
*/
}在浏览器,默认情况下,this 是指向 window 对象的,而 window 对象是常驻内存的,所以即便 foo 函数退出了,但是 temp_array 依然被 window 对象引用了, 所以 temp_array 依然也会和 window 对象一样,会常驻内存。因为 temp_array 已经是不再被使用的对象了,但是依然被 window 对象引用了,这就造成了 temp_array 的泄漏。
为了解决这个问题,我们可以在 JavaScript 文件头部加上 'use strict',使用严格模式避免意外的全局变量,此时上例中的 this 指向 undefined。
另外,我们还要时刻警惕闭包这种情况,因为闭包会引用父级函数中定义的变量,如果引用了不被需要的变量,那么也会造成内存泄漏。比如看下面这样一段代码:
function foo(){
var temp_object = new Object()
temp_object.x = 1
temp_object.y = 2
temp_object.array = new Array(200000)
/**
* 使用temp_object
*/
return function(){
console.log(temp_object.x);
}
}可以看到,foo 函数使用了一个局部临时变量 temp_object,temp_object 对象有三个属性,x、y,还有一个非常占用内存的 array 属性。最后 foo 函数返回了一个匿名函数,该匿名函数引用了 temp_object.x。那么当调用完 foo 函数之后,由于返回的匿名函数引用了 foo 函数中的 temp_object.x,这会造成 temp_object 无法被销毁,即便只是引用了 temp_object.x,也会造成整个 temp_object 对象依然保留在内存中。
可以这样改造下这段代码:
function foo(){
var temp_object = new Object()
temp_object.x = 1
temp_object.y = 2
temp_object.array = new Array(200000)
/**
* 使用temp_object
*/
let closure = temp_object.x
return function(){
console.log(closure);
}
}再来看看由于 JavaScript 引用了 DOM 节点而造成的内存泄漏的问题,只有同时满足 DOM 树和 JavaScript 代码都不引用某个 DOM 节点,该节点才会被作为垃圾进行回收。 如果某个节点已从 DOM 树移除,但 JavaScript 仍然引用它,称此节点为“detached”。“detached ”节点是 DOM 内存泄漏的常见原因。比如下面这段代码:
let detachedTree;
function create() {
var ul = document.createElement('ul');
for (var i = 0; i < 100; i++) {
var li = document.createElement('li');
ul.appendChild(li);
}
detachedTree = ul;
}
create()
通过 JavaScript 创建了一些 DOM 元素,有了这些内存中的 DOM 元素,当有需要的时候,我们就快速地将这些 DOM 元素关联到 DOM 树上,一旦这些 DOM 元素从 DOM 上被移除后,它们并不会立即销毁,这主要是由于 JavaScript 代码中保留了这些元素的引用,导致这些 DOM 元素依然会呆在内存中。所以在保存 DOM 元素引用的时候,我们需要非常小心谨慎。
内存膨胀(Memory bloat) #
内存膨胀和内存泄漏有一些差异,内存膨胀主要表现在程序员对内存管理的不科学,比如只需要 50M 内存就可以搞定的,有些程序员却花费了 500M 内存。
额外使用过多的内存有可能是没有充分地利用好缓存,也有可能加载了一些不必要的资源。通常表现为内存在某一段时间内快速增长,然后达到一个平稳的峰值继续运行。
比如一次性加载了大量的资源,内存会快速达到一个峰值。内存膨胀和内存泄漏的关系可以参看下图:

我们可以看到,内存膨胀是快速增长,然后达到一个平衡的位置,而内存泄漏是内存一直在缓慢增长。要避免内存膨胀,我们需要合理规划项目,充分利用缓存等技术来减轻项目中不必要的内存占用。
频繁的垃圾回收 #
频繁使用大的临时变量,导致了新生代空间很快被装满,从而频繁触发垃圾回收。频繁的垃圾回收操作会让你感觉到页面卡顿。比如下面这段代码:
function strToArray(str) {
let i = 0
const len = str.length
let arr = new Uint16Array(str.length)
for (; i < len; ++i) {
arr[i] = str.charCodeAt(i)
}
return arr;
}
function foo() {
let i = 0
let str = 'test V8 GC'
while (i++ < 1e5) {
strToArray(str);
}
}
foo()这段代码就会频繁创建临时变量,这种方式很快就会造成新生代内存内装满,从而频繁触发垃圾回收。为了解决频繁的垃圾回收的问题,可以考虑将这些临时变量设置为全局变量。