首先,我们来实现一个例子
- 有一个common.js文件,文件中有一个isNumber方法,这个方法被导出。
js
export function isNumber(value) {
return typeof value === 'number'
}
- 有一个tools.js文件,文件中有add和sub方法,这两个方法都被导出。 同时,这个文件引入并使用了common.js中的isNumber方法
js
import { isNumber } from './common'
export function add(a, b) {
if (!isNumber(a) || !isNumber(b)) return 0
return a + b
}
export function sub(a, b) {
if (!isNumber(a) || !isNumber(b)) return 0
return a - b
}
- 有一个index.js文件,这个文件引入并使用了tools.js文件中的add方法,也引入和使用了common.js文件中的isNumber方法。 这个文件是入口文件。
js
import { add, sub } from './tools'
import { isNumber } from './common'
function component(a, d){
const ele = document.createElement('div')
if (!isNumber(a) || !isNumber(b)) return
ele.innerHTML = add(a, b);
return ele
}
document.body.appendChild(component(a, b))
- 有一个login.js文件,这个文件引入并使用了tools文件的add方法,也引入和使用了common.js文件中的isNumber方法。这个文件是另一个入口文件。
js
import { add, sub } from './tools'
import { isNumber } from './common'
function component(a, d){
const ele = document.createElement('div')
if (!isNumber(a) || !isNumber(b)) return
ele.innerHTML = add(a, b).toString() + 'login';
return ele
}
document.body.appendChild(component(a, b))
通过上面的例子我们来讲解下webpack的tree shaking实现过程
moduleGraph
首先,这些文件解析后,将所有文件的信息存放在moduleGraph对象中。
moduleGraph._moduleMap
moduleGraph对象上有一个_moduleMap数组,数组的项是一个module对象。
_moduleMap[n].key.dependencies
module对象key上面有一个dependencies数组,该数组上存放着当前module依赖的文件和方法, HarmonyImportSideEffectDependency是文件,HarmonyImportSpecifierDependency是方法。
_moduleMap[n].value.exports._exports
module对象value上有一个exports对象,exports._exports对象上存放当前module导出的属性和方法。
首先,执行入口文件index.js, index.js文件引入了tools.js文件中的add和sub方法,但sub方法实际是没有使用的,所以index.js文件对应的module对象上的dependencies数组里面没有sub方法。
webpack通过dependence上记录的绝对路径找到依赖的tools.js文件,再从tools.js对应的module对象上的exports上找到tools.js导出的所有属性和方法。index.js文件引入并使用了tools.js文件上的add方法,所以会在exports.add下的_usedInRuntime中添加一条记录。
核心源码
js
getDependencyReferencedExports(dependency, runtime) {
const referencedExports = dependency.getReferencedExports(
this.moduleGraph,
runtime
);
return this.hooks.dependencyReferencedExports.call(
referencedExports,
dependency,
runtime
);
}
// 这个函数用于给_usedInRuntime赋值
setUsedConditionally(condition, newValue, runtime) {
if (runtime === undefined) {
if (this._globalUsed === undefined) {
this._globalUsed = newValue;
return true;
} else {
if (this._globalUsed !== newValue && condition(this._globalUsed)) {
this._globalUsed = newValue;
return true;
}
}
} else if (this._usedInRuntime === undefined) {
if (newValue !== UsageState.Unused && condition(UsageState.Unused)) {
this._usedInRuntime = new Map();
forEachRuntime(runtime, runtime =>
this._usedInRuntime.set(runtime, newValue)
);
return true;
}
} else {
let changed = false;
forEachRuntime(runtime, runtime => {
/** @type {UsageStateType} */
let oldValue = this._usedInRuntime.get(runtime);
if (oldValue === undefined) oldValue = UsageState.Unused;
if (newValue !== oldValue && condition(oldValue)) {
if (newValue === UsageState.Unused) {
this._usedInRuntime.delete(runtime);
} else {
this._usedInRuntime.set(runtime, newValue);
}
changed = true;
}
});
if (changed) {
if (this._usedInRuntime.size === 0) this._usedInRuntime = undefined;
return true;
}
}
return false;
}
没有被使用的代码被标记
在webpack.config.js文件中,我们设置mode: 'development'
,mode为development的情况下,默认是不进行tree shaking的。我们再设置optimization: {usedExports: true}
,这个配置是启动标记功能。两个配置最终达到的效果是只标记不tree shaking。
现在我们分析下标记后输出的文件内容:
如上图,我们看到没有使用的方法会/* unused harmony export sub */
这样描述,并且没有使用的方法没有被导出。
terser
经过分析terser源码发现,terser并不是通过/* unused harmony export */
这样的描述来剔除代码的,而是通过分析语句来实现的。
下面是一个例子
首先,有下面一段代码,我们用terser压缩下。
js
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ add: () => (/* binding */ add)
/* harmony export */ });
/* harmony import */ var _common__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./common */ "./src/common.js");
function add(a, b) {
if (!(0,_common__WEBPACK_IMPORTED_MODULE_0__.isNumber)(a) || !(0,_common__WEBPACK_IMPORTED_MODULE_0__.isNumber)(b)) return 0
return a + b
}
function sub(a, b) {
if (!(0,_common__WEBPACK_IMPORTED_MODULE_0__.isNumber)(a) || !(0,_common__WEBPACK_IMPORTED_MODULE_0__.isNumber)(b)) return 0
return a - b
}
//# sourceURL=webpack://webpack-demo/./src/tools.js?
terser的测试脚本如下:
js
const { minify } = require("terser");
const fs = require('fs')
async function test() {
var result = await minify(fs.readFileSync("src/index.js", "utf8"), {
compress: { //(默认 {})------传递false以完全跳过压缩。传递一个对象以指定自定义 压缩选项
dead_code: true,
unused: true,
global_defs: {
DEBUG: false
},
},
mangle: true, // (默认true)------传递false以禁用名称混淆,或传递一个对象以指定mangle选项
ecma: 5,
toplevel: true, //(默认false)------设置为true以启用顶级变量和函数名混淆即未使用的变量和函数
parse: { // (默认{})------------------如果需要指定更多解析选项,请传递一个对象。
bare_returns: true
}
});
console.log(result)
}
test()
terser分析这段代码得到的最外层有四条语句
terser分析第四条语句,也就是sub函数
js
// 1. 首先这条语句的原型是AST_Defun
if (node instanceof AST_Defun || node instanceof AST_DefClass) {
var node_def = node.name.definition();
// 2. 判断它是否被导出
const in_export = tw.parent() instanceof AST_Export;
// 3. 只有被导出的函数,才会将它的id放入in_use_ids,不在in_use_ids中的语句会被terser剔除。
if (in_export || !drop_funcs && scope === self) {
if (node_def.global) {
in_use_ids.set(node_def.id, node_def);
}
}
map_add(initializations, node_def.id, node);
return true; // don't go in nested scopes
}
tw.parent()
js
parent(n) {
return this.stack[this.stack.length - 2 - (n || 0)];
}
this.stack
栈顶是AST_Toplevel
我们再来分析下add函数是怎么被识别出被使用了的?
js
__webpack_require__.d(__webpack_exports__, {
add: () => (/* binding */ add)
});
add在箭头函数中被使用
js
if (node instanceof AST_SymbolRef) {
node_def = node.definition();
if (!in_use_ids.has(node_def.id)) {
in_use_ids.set(node_def.id, node_def);
if (node_def.orig[0] instanceof AST_SymbolCatch) {
const redef = node_def.scope.is_block_scope()
&& node_def.scope.get_defun_scope().variables.get(node_def.name);
if (redef) in_use_ids.set(redef.id, redef);
}
}
return true;
}