前端之数据结构和算法 | 青训营笔记

91 阅读3分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 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算法(最近最少使用原则),即把最近使用频率最低的节点删除掉,尽可能保留使用频率高的缓存,这个算法可以应对绝大多数情况了。