这是我参与「第五届青训营 」伴学笔记创作活动的第 11 天
数据结构和算法并不是只存在于象牙塔里面的题目和模型,它在我们的技术栈和专业领域中无处不在,和我们的日常开发息息相关,是需要我们穷尽整个职业生涯去追求和探索的东西。本文将从实战的角度出发,通过案例来讲解实际工程中所接触到的数据结构和算法。
遍历所有文件
用Node.js扫描指定目录下的所有文件。
const fs = require( 'fs ');
function scanDirectory(directoryPath) {
let allFiles = [];
function readDir(path) {
let files = fs.readdirsync(path);
files.forEach((file) => {
let fullPath = path + "/" + file;
let stats = fs.statsync(fullPath);
if (stats.isDirectory()) {
readDir(fullPath);
} else {
allFiles.push(fullPath);
}
});
}
readDir(directoryPath);
return allFiles;
}
上面是一个简单的递归程序,使用递归可以实现不重不漏的枚举。readdirSync 是 Node.js 中 fs 模块提供的同步函数,用于读取指定目录下的所有文件和子目录。statSync是一个同步函数,用于获取指定路径的文件信息对象。isDirectory用于判断指定路径是否为一个目录。
AST解析器
AST即抽象语法树,通过AST解析器,我们可以将代码解析为一棵对象树的结构,每个语法都对应树里面的某些节点。这样,我们可以很方便地进行语法分析和转换。无论是HTML/CSSI/JS,背后的解析算法都是差不多的。
对于AST解析器而言,最重要的两步是: tokenize(词法分析)和parse(语法分析)。
在语法解析阶段,我们会逐个遍历token,然后构造出一个完整的对象树结构,伪代码如下:
// 以let foo = function() {} 代码为例
function parse( ) {
// 消费let这个token
consume('let');
// 通过Let后面的变量名foo构造Identifier节点
const id = this._parseIdentifier();
// 消费 = 这个token
consume( '=');
// 解析函数表达式,生成相应节点
const init = this._parseFunctionExpression();
const declarator: VariableDeclarator = {
type: NodeType.VariableDeclarator,
id,
init,
};
return declarator;
}
缓存淘汰处理
在日常开发中,我们会经常使用缓存来优化性能。比如如下的几个场景:
- SSR缓存,服务端将组件渲染为字符串之后缓存 html结果,这样在下次访问时可以直接读取内存中的缓存,跳过了CPU密集型操作。
- Vue的KeepAlive组件缓存了内部组件的实例,避免了再次渲染时重新创建组件、请求数据的开销。
- webpack 中对 loader的结果进行缓存,在二次启动时可以跳过loader 的处理,大幅提升构建速度。
而一旦涉及缓存,往往就需要考虑缓存淘汰的问题,比如我们将SSR的结果缓存到内存中,内存是有限的,而过多的缓存内容可能会导致我们的服务稳定性出现问题。
一般情况下,我们会设置一个阈值,然后在缓存大小超过阈值的时候选择将某些缓存节点进行删除。
比较常用的是LRU算法(最近最少使用原则),即把最近使用频率最低的节点删除掉,尽可能保留使用频率高的缓存,这个算法可以应对绝大多数情况了。