面试官:tree-shaking的原理是什么?

news2024/11/18 23:33:58

公众号:程序员白特,欢迎一起交流学习~

原文:https://juejin.cn/post/7265125368553685050?share_token=301335cf-a17e-4115-94f0-264fe0e82f07

前言

在前端面试的过程中,前端工程化一直是考察面试者能力的一个重点,而在我们常用的项目打包工具中,无论是webpack还是rollup,都具备一项很强大的能力——tree-shaking,所以面试官也常常会问到tree-shaking的原理是什么,这时我们该如何回答呢?其实核心原理就是AST

在前端开发中,其实AST的应用有很多,比如我们前端项目的打包工具webpack、代码检查工具Eslint,代码转换工具babel都依赖了AST的语法分析和转换能力。

AST简单介绍

ASTAbstract Syntax Tree的缩写,这玩意儿的全称叫抽象语法树,它可以用来描述我们代码的语法结构

举个例子:

// ast.js
let a = 1;
function add() {}

我这里创建了一个文件ast.js,可以把整个文件理解为一个File节点,存放程序体,而里面就是我们javascript的语法节点了,我们javascript语法节点的根节点是Program,而我们在里面定了了两个节点,第一个节点是let a = 1,解析为ast是VariableDeclaration,也就是变量声明节点,第二个节点是function add() {},解析为ast是FunctionDeclaration,也就是函数声明节点

这里给大家推荐一个平台:AST Explorer,能很清晰看到JavaScript代码转化为AST后的结果,看下下面这张图就一目了然了:

AST的作用

那拿到了代码的ast信息我们能做什么呢?

  1. 代码分析与转换AST可以将我们的代码解析成一棵ast树,我们自然可以将这棵树进行处理和转换,这个最经典的应用莫过于babel了,将我们的高级语法ES6转换为ES5后,然后再把ast树转换成代码输出。除此之外,webpack的处理ES6的import和export也是依赖了ast的能力,以及我们的jsx的语法转换等。
  2. 语法检查和错误提示。我们把语法解析成ast树之后,自然就可以按照一定的语法规则去检查它的语法是否正确,一旦错误就可以抛出错误,提醒开发者去修正。比如我们使用的vscode就是利用AST 提供实时的语法检查和错误提示。而在前端项目中,应用的最广的语法检查工具就是ESlint了,基本就是前端项目必备。
  3. 静态类型检查。这个其实跟第二点有点像,不过第二点是侧重于语法检查,而这个是针对类型检查的,比如我们的Typescript会利用ast进行类型检查和推断。
  4. 代码重构。基于AST树,我们可以对代码进行自动重构。比如提取函数、变量重命名、语法升级、函数移动等。

其实在实际开发中,我们也可以利用做很多的事情,比如实现自动埋点自动国际化依赖分析和治理等等,有兴趣的小伙伴可以自行去探索。

而今天我主要介绍的就是AST的一大应用,就是我们webpack强大的Tree-Shaking能力。

Tree-shaking

Tree-shaking翻译过来的意思就是摇树。这棵树可以把它比喻为现实中的树,可以这样理解,摇树就是把发黄、没有作用还要汲取养分的叶子给给摇掉。把这个搬到javascript程序里面来就是,移除Javascript上下文中的未引用代码(dead-code)

废话不多说,直接看例子:

// test.js
function add(a, b) {
 return a + b;
}
function multiple(a, b) {
 return a * b;
}
let firstNum = 3, secondNum = 4;
add(firstNum, secondNum);

在这段代码中,我们定义了两个函数addmultiple,两个变量firstNumsecondNum,然后调用了add方法并将firstNumsecondNum作为参数传入。

很明显,multiple方法是没有被调用到的,打包的时候其实是可以被删除掉的,以减少我们打包后的代码体积。

那么,如何删除multiple呢?这时候就该我们的ast就登场了!要实现这个功能,分三步走。

第一步:解析源代码生成ast

先看如下代码:

const acorn = require('acorn');
const fs = require('fs');
const path = require('path');
const buffer = fs.readFileSync(path.resolve(process.cwd(), 'test.js')).toString();
const body = acorn.parse(buffer, {
 ecmaVersion: 'latest',
}).body;

这里我们选中acorn(babel其实是基于acorn实现解析器的)来对我们的代码进行解析,运行前需要先执行npm install acorn安装下acorn,然后读取文件内容传入acorn,得到ast

我们可以用AST Explorer,来看一眼我们现在的AST

第二步:遍历ast,记录相关信息

一、先明确下我们要记录哪些信息?

我们的主要目的是收集到未引用的代码,然后将它们删除掉,所以我们最容易想到的需要收集的信息有两个:

  1. 收集所有的函数或变量类型节点
  2. 收集所有使用过的函数或变量类型节点

那我们先试试看:

const acorn = require('acorn');
const fs = require('fs');
const path = require('path');
const buffer = fs.readFileSync(path.resolve(process.cwd(), './src/index.js')).toString();
const body = acorn.parse(buffer, {
 ecmaVersion: 'latest',
}).body;
// 引用一个 Generator 类,用来生成 ast 对应的代码
const Generator = require('./generator');
// 创建 Generator 实例
const gen = new Generator();
// 定义变量decls  存储所有的函数或变量类型节点 Map类型
const decls = new Map();
// 定义变量calledDecls 存储被用到过的函数或变量类型节点 数组类型
const calledDecls = [];

这里我引入了一个Generator,其作用就是将每个ast节点转化成对应的代码,来看下Generator的实现:

  1. 先定义好Generator类,将其导出。
// generator.js
class Generator {
}
module.exports = Generator;
  1. 定义run方法以及visitNodevisitNodes方法。
  • run:调用visitNodes方法生成代码。
  • visitNode:根据节点类型,调用对应的方法进行对应处理。
  • visitNodes:就是处理数组类型的节点用的,内部循环调用了visitNode方法。
// generator.js
class Generator {
     run(body) {
        let str = '';
        str += this.visitNodes(body);
        return str;
    }
    visitNodes(nodes) {
        let str = '';
        for (const node of nodes) {
            str += this.visitNode(node);
        }
        return str;
    }
    visitNode(node) {
        let str = '';
        switch (node.type) {
            case 'VariableDeclaration':
                str += this.visitVariableDeclaration(node);
                break;
            case 'VariableDeclarator':
                str += this.visitVariableDeclarator(node);
                break;
            case 'Literal':
                str += this.visitLiteral(node);
                break;
            case 'Identifier':
                str += this.visitIdentifier(node);
                break;
            case 'BinaryExpression':
                str += this.visitBinaryExpression(node);
                break;
            case 'FunctionDeclaration':
                str += this.visitFunctionDeclaration(node);
                break;
            case 'BlockStatement':
                str += this.visitBlockStatement(node);
                break;
            case 'CallExpression':
                str += this.visitCallExpression(node);
                break;
            case 'ReturnStatement':
                str += this.visitReturnStatement(node);
                break;
            case 'ExpressionStatement':
                str += this.visitExpressionStatement(node);
                break;
        }
        return str;
    }
}
  1. 再分别介绍下每个处理节点类型的方法实现

实现visitVariableDeclaration方法

class Generator {
 // 省略xxx
 visitVariableDeclaration(node) {
   let str = '';
   str += node.kind + ' ';
   str += this.visitNodes(node.declarations);
   return str + '\n';
 }
 // 省略xxx
}

visitVariableDeclaration就是处理let firstNum = 3这种变量声明的节点,node.kind就是let/const/var,然后变量可以一次声明多个,比如我们test.js里面写的let firstNum = 3, secondNum = 4;,这样node.declarations就有两个节点了。

class Generator {
 // 省略xxx
 visitVariableDeclaration(node) {
 let str = '';
 str += node.kind + ' ';
 str += this.visitNodes(node.declarations);
 return str + '\n';
 }
 // 省略xxx
}

实现visitVariableDeclarator方法

class Generator {
   // 省略xxx
  visitVariableDeclaration(node) {
    let str = '';
    str += node.kind + ' ';
    str += this.visitNodes(node.declarations);
    return str + '\n';
  }
   // 省略xxx
}

visitVariableDeclarator就是上面VariableDeclaration的子节点,可以接收到父节点的kind,比如let firstNum = 3它的id就是变量名firstNum,init就是初始化的值3

实现visitLiteral方法

class Generator {
 visitLiteral(node) {
   return node.raw;
 }
}

Literal字面量,比如let firstNum = 3里面的3就是一个字符串字面量,除此之外,还有数字字面量布尔字面量等等,这个直接返回它的raw属性就行啦。

实现visitIdentifier方法

class Generator {
 visitIdentifier(node) {
   return node.name;
 }
}

Identifier标识符,比如变量名、属性名、参数名等都是,比如let firstNum = 3firstNum,直接返回它的name属性即可。

实现visitBinaryExpression方法

BinaryExpression二进制表达式,比如我们的加减乘除运算都是,比如a + b的ast如下:

我们需要拿到它的左右节点和中间的标识符拼接起来。

class Generator {
 visitBinaryExpression(node) {
   let str = '';
   str += this.visitNode(node.left);
   str += node.operator;
   str += this.visitNode(node.right);
   return str + '\n';
 }
}

实现visitFunctionDeclaration方法

FunctionDeclaration即为函数声明节点,稍微复杂一些,因为我们要拼接一个函数出来。

function add(a, b) {
 return a + b;
}

比如我们这个add函数转成ast如下图:

class Generator {
 visitFunctionDeclaration(node) {
   let str = 'function ';
   str += this.visitNode(node.id);
   str += '(';
   for (let paramIndex = 0; paramIndex < node.params.length; paramIndex++) {
     str += this.visitNode(node.params[paramIndex]);
     str += ((node.params[paramIndex] === undefined) ? '' : ',')
   }
   str = str.slice(0, str.length - 1);
   str += '){\n';
   str += this.visitNode(node.body);
   str += '}';
   return str + '\n';
 }
}

先拿到node.id,即add,然后处理函数的参数params,由于存在多个params需要循环处理,用逗号拼接,然后再调用visitNode方法拼接node.body函数体

实现visitBlockStatement方法

BlockStatement块语句,就是我们的大括号包裹的部分。比如add函数里面的块语句的ast如下:

class Generator {
 visitBlockStatement(node) {
   let str = '';
   str += this.visitNodes(node.body);
   return str;
 }
}

只需要用visitNodes函数拼接它的node.body即可。

实现visitCallExpression方法

CallExpression也就是函数调用,比如add(firstNum, secondNum),它比较重要的属性有:

  • callee:也就是 add
  • arguments:也就是调用的参数 firstNumsecondNum 其ast如下:

class Generator {
 visitCallExpression(node) {
   let str = '';
   str += this.visitIdentifier(node.callee);
   str += '(';
   for (const arg of node.arguments) {
     str += this.visitNode(arg) + ',';
   }
   str = str.slice(0, -1);
   str += ');';
   return str + '\n';
 }
}

只需要将它的callee以及参数arguments处理好用小括号()拼接起来即可。

实现visitReturnStatement方法

ReturnStatement返回语法,比如return a + b这种,它的ast如下:

它的实现也比较简单,直接拼接node.argument就好啦:

class Generator {
 visitReturnStatement(node) {
   let str = '';
   str = str + '  return ' + this.visitNode(node.argument);
   return str + '\n';
 }
}

实现visitExpressionStatement方法

ExpressionStatement表达式语句,它的特点是执行完之后有返回值,比如add(firstNum, secondNum);,它是在CallExpression外面包裹了一层ExpressionStatement,执行完之后返回函数的调用结果,其ast如下:

所以实现也比较简单,我们只需要处理它的expression返回就好了。

class Generator {
 return this.visitNode(node.expression);
}

这样,我们就完整实现Generator了,接下来就可以开始遍历ast了。

// tree-shaking.js
const acorn = require('acorn');
const fs = require('fs');
const path = require('path');
const buffer = fs.readFileSync(path.resolve(process.cwd(), './src/index.js')).toString();
const body = acorn.parse(buffer, {
    ecmaVersion: 'latest',
}).body;
// 引用一个 Generator 类,用来生成 ast 对应的代码
const Generator = require('./generator');
// 创建 Generator 实例
const gen = new Generator();
// 定义变量decls  存储所有的函数或变量类型节点 Map类型
const decls = new Map();
// 定义变量calledDecls 存储被用到过的函数或变量类型节点 数组类型
const calledDecls = [];
// 开始遍历 ast
body.forEach(node => {
    if (node.type === 'FunctionDeclaration') {
        const code = gen.run([node]);
        decls.set(gen.visitNode(node.id), code);
        return;
    }
    if (node.type === 'VariableDeclaration') {
        for (const decl of node.declarations) {
            decls.set(gen.visitNode(decl.id), gen.visitVariableDeclarator(decl, node.kind));
        }
        return;
    }
    if (node.type === 'ExpressionStatement') {
        if (node.expression.type === 'CallExpression') {
            const callNode = node.expression;
            calledDecls.push(gen.visitIdentifier(callNode.callee));
            for (const arg of callNode.arguments) {
                if (arg.type === 'Identifier') {
                    calledDecls.push(arg.name);
                }
            }
        }
    }
    if (node.type === 'Identifier') {
        calledDecls.push(node.name);
    }
})
console.log('decls', decls);
console.log('calledDecls', decls);

使用node tree-shaking.js运行一下,结果如下:

很明显,我们decls总共有四个节点函数或变量类型节点,而被调用的calledDecls只有三个,很明显multiple函数没被调用,可以被tree-shaking掉,拿到这些信息之后,接下来我们开始生成tree-shaking后的代码。

第三步:根据第二步得到的信息,生成新代码

// ...省略
code = calledDecls.map(c => decls.get(c)).join('');
console.log(code);

我们直接遍历calledDecls生成新的源代码,打印结果如下:

咦!跟我们最开始的文件内容一比,发现multiple虽然被移除掉了,但我们的函数调用语句add(firstNum, secondNum);却丢了,那我们简单处理下。

我们声明一个code数组:

// ...省略
const calledDecls = [];
// 保存代码信息
const code = [];
// 省略

然后把不是FunctionDeclarationVariableDeclaration的信息都存储一下:

// tree-shaking.js
body.forEach(node => {
    if (node.type === 'FunctionDeclaration') {
        const code = gen.run([node]);
        decls.set(gen.visitNode(node.id), code);
        return;
    }
    if (node.type === 'VariableDeclaration') {
        for (const decl of node.declarations) {
            decls.set(gen.visitNode(decl.id), gen.visitVariableDeclarator(decl, node.kind));
        }
        return;
    }
    if (node.type === 'ExpressionStatement') {
        if (node.expression.type === 'CallExpression') {
            const callNode = node.expression;
            calledDecls.push(gen.visitIdentifier(callNode.callee));
            for (const arg of callNode.arguments) {
                if (arg.type === 'Identifier') {
                    calledDecls.push(arg.name);
                }
            }
        }
    }
    if (node.type === 'Identifier') {
        calledDecls.push(node.name);
    }
    // 存储代码信息
    code.push(gen.run([node]));
})

最后输出的时候,把code里面的信息也带上:

// tree-shaking.js
code = calledDecls.map(c => decls.get(c)).concat(code).join('');
console.log(code);

然后运行一下,打印结果如下:

这样我们一个简易版本的tree-shaking就完成了,当然,webpack的tree-shaking的能力远比这个强大的多,我们只是写了个最简单版本,实际项目要比这复杂得多:

  • 处理文件依赖import/export
  • 作用域scope的处理
  • 递归tree-shaking,因为可能去除了某些代码后,又会产生新的未被引用的代码,所以需要递归处理
  • 等等…

小结

本文通过ast的语法分析能力,分析JavaScript代码中未被引用的函数或变量,进而实现了一个最简单版本的tree-shaking,希望大家看完都能有所收获哦~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1547953.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

nginx代理服务后,有关文件的操作无法执行,nginx代理jupyter或为知笔记后无法创建文件及文件夹,无法操作文件

nginx配置 server {listen 18001; # 修改转发的接口listen [::]:18001; # 修改转发的接口server_name _;root /usr/share/nginx/html;location / {proxy_pass http://127.0.0.1:7777; # 指定自己服务地址proxy_set_header Host $host;}# Load configurat…

【C语言】Infiniband驱动__mlx4_init_one函数

一、注释 Linux内核驱动程序中的部分&#xff0c;属于Mellanox网卡驱动mlx4的初始化过程。 // Mellanox 以太网驱动主程序代码 static int __mlx4_init_one(struct pci_dev *pdev, int pci_dev_data,struct mlx4_priv *priv) {int err; // 错误码变量int nvfs[MLX4_MAX_PORTS…

【C++】从C到C++、从面向过程到面向对象(类与对象)

文章目录 C入门知识C与C的关系1. 类的引入&#xff1a;从结构体到类2. 类的声明和定义3. 类的作用域4. 类的访问限定符5. 面向对象特性之一&#xff1a;封装6. 类的实例化&#xff1a;对象7. 计算类对象的内存大小8. 成员函数中暗藏的this指针9. 类的六个默认生成的成员函数9.1…

【二叉树】Leetcode 226. 翻转二叉树【简单】

翻转二叉树 给你一棵二叉树的根节点 root &#xff0c;翻转这棵二叉树&#xff0c;并返回其根节点。 示例 1&#xff1a; 输入&#xff1a;root [4,2,7,1,3,6,9] 输出&#xff1a;[4,7,2,9,6,3,1] 解题思路 二叉树翻转操作是指将二叉树中每个节点的左右子树进行交换。具体…

[Android]模拟器登录Google Play失败

问题&#xff1a; 模拟器登录Google Play失败&#xff0c;提示couldnt sign in there was a problem communicating with google servers. try again later. 原因&#xff1a; 原因是模拟器没有连接到互联网&#xff0c;打开模拟器中Google浏览器进行搜索一样不行。 解决&am…

虚拟机如何在原有磁盘上扩容

虚拟机未开启状态–菜单栏–虚拟机–快照–拍摄快照–拍摄快照– 菜单栏–虚拟机–快照–快照管理器–点击刚刚的快照1–删除–是– 文件–新建或者打开–硬盘&#xff08;以本人Win 10.64.3GL为例&#xff09;–虚拟机设置–硬件– 硬盘&#xff08;SATA&#xff09;–磁盘实…

赛氪网亮相中国人工智能产业发展联盟会议,共筑赛事生态新篇章

2024年3月14日至15日&#xff0c;备受瞩目的中国人工智能产业发展联盟&#xff08;AIIA&#xff09;第十一次全体会议在海南海口盛大召开。作为人工智能领域的重要交流与合作平台&#xff0c;此次会议吸引了300余位联盟成员单位代表齐聚一堂&#xff0c;共襄盛举。在这场人工智…

梵宁教育:助力大学生掌握设计新技能

在当今数字化、信息化的社会里&#xff0c;设计技能的重要性日益凸显。设计不仅仅局限于美术或艺术领域&#xff0c;它已经渗透到各行各业&#xff0c;从产品外观到用户界面&#xff0c;从品牌形象到营销策划&#xff0c;设计无处不在。对于大学生而言&#xff0c;掌握设计新技…

ts enum elementUI 表格列表中如何方便的标识不同类型的内容,颜色区分 enum ts + vue3

ts enum elementUI 表格列表中如何方便的标识不同类型的内容&#xff0c;颜色区分 enum ts vue3 本文内容为 TypeScript 一、基础知识 在展示列表的时候&#xff0c;列表中的某个数据可能是一个类别&#xff0c;比如&#xff1a; enum EnumOrderStatus{"未受理" …

python如何获取word文档的总页数

最近在搞AI. 遇到了一个问题&#xff0c;就是要进行doc文档的解析。并且需要展示每个文档的总页数。 利用AI. 分别尝试了chatGPT, 文心一言&#xff0c; github copilot&#xff0c;Kimi 等工具&#xff0c;给出来的答案都不尽如人意。 给的最多的查询方式就是下面这种。 这个…

时序预测 | Matlab实现GWO-BP灰狼算法优化BP神经网络时间序列预测

时序预测 | Matlab实现GWO-BP灰狼算法优化BP神经网络时间序列预测 目录 时序预测 | Matlab实现GWO-BP灰狼算法优化BP神经网络时间序列预测预测效果基本介绍程序设计参考资料 预测效果 基本介绍 1.Matlab实现GWO-BP灰狼算法优化BP神经网络时间序列预测&#xff08;完整源码和数据…

kali系统下载,并在VMware免安装式打开kali虚拟机

Kali Linux | Penetration Testing and Ethical Hacking Linux Distribution 上面连接为官网连接 此安装过程也适合其他虚拟机的安装&#xff0c;也就是你曾经安装过的虚拟机&#xff0c;会保存一推文件&#xff0c;而这推文件&#xff0c;还可以用VMware再次打开。

Unity中如何实现草的LOD

1&#xff09;Unity中如何实现草的LOD 2&#xff09;用Compute Shader处理图像数据后在安卓机上不能正常显示渲染纹理 3&#xff09;关于进游戏程序集加载的问题 4&#xff09;预制件编辑模式一直在触发自动保存 这是第379篇UWA技术知识分享的推送&#xff0c;精选了UWA社区的热…

【Linux】线程的概念{虚拟地址堆区细分/缺页中断/页/初识线程/创建线程/优缺点}

文章目录 1.前导知识1.1 虚拟地址空间的堆区1.2 缺页中断1.3ELF文件格式1.4页/页框/页帧/页表/MMU1.5虚拟地址到物理地址 2.初识Linux线程2.1之前所学的进程2.2线程的引入2.3如何理解线程2.4如何理解轻量级进程 3.创建线程3.1pthread_create()函数3.2程序测试3.3Makefile怎么写…

力扣--并查集1631.最小体力消耗路径

这题将图论和并查集联系起来。把数组每个位置看成图中的一个节点。 这段代码的主要思路是&#xff1a; 遍历地图中的每个节点&#xff0c;将每个节点与其相邻的下方节点和右方节点之间的边加入到边集合中&#xff08;因为从上到下和从下到上他们高度绝对值一样的&#xff0c;…

文件IO的方式读取jpeg图片的分辨率

1、读取jpeg图片分辨率的两种方式 1.1 使用libjpeg库 可以使用libjpeg库读取JPEG图像文件&#xff0c;并获取图像的分辨率&#xff08;宽度和高度&#xff09;&#xff0c;简单demo示例如下&#xff1a; #include <stdio.h> #include <jpeglib.h>int main() {st…

6、ChatGLM3-6B 部署实践

一、ChatGLM3-6B介绍与快速入门 ChatGLM3 是智谱AI和清华大学 KEG 实验室在2023年10月27日联合发布的新一代对话预训练模型。ChatGLM3-6B 是 ChatGLM3 系列中的开源模型&#xff0c;免费下载&#xff0c;免费的商业化使用。 该模型在保留了前两代模型对话流畅、部署门槛低等众多…

GPT2从放弃到入门(四)

引言 体验地址&#xff1a;https://huggingface.co/spaces/greyfoss/gpt2-chatbot 上篇文章我们通过Gradio作为前端轻松地连接到训练好的Chatbot&#xff0c;本文介绍如何分享你创建好的模型给你的朋友。 当你训练好的模型推送到Huggingface Hub上后&#xff0c;其实还可以进一…

How to convert .py to .ipynb in Ubuntu 22.04

How to convert .py to .ipynb in Ubuntu 22.04 jupyter nbconvertp2j 最近看到大家在用jupyter notebook&#xff0c;我也试了一下&#xff0c;感觉还不错&#xff0c;不过&#xff0c;也遇到了一些问题&#xff0c;比方说&#xff0c;我有堆的.py文件&#xff0c;如果要一个一…

YOLOv8融入低照度图像增强算法---传统算法篇

YOLOv8n原图检测YOLOv8n增强后检测召回率和置信度都有提升 前言 这篇博客讲讲低照度,大家都催我出一些内容,没想到这么多同学搞这个,恰好我也做过这方面的一些工作,那今天就来讲解一些方法,低照度的图像增强大体分“传统算法”和“深度学习算法”; 目前低照度的图像增…