webpack的tree shaking实现过程

首先,我们来实现一个例子

  1. 有一个common.js文件,文件中有一个isNumber方法,这个方法被导出。
js 复制代码
export function isNumber(value) {
    return typeof value === 'number'
}
  1. 有一个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
}
  1. 有一个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))
  1. 有一个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

gitcode.com/terser/ters...

经过分析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;
        }

参考:

mp.weixin.qq.com/s/McigcfZyI...

相关推荐
m0_7482550213 分钟前
前端常用算法集合
前端·算法
真的很上进27 分钟前
如何借助 Babel+TS+ESLint 构建现代 JS 工程环境?
java·前端·javascript·css·react.js·vue·html
web1309332039833 分钟前
vue elementUI form组件动态添加el-form-item并且动态添加rules必填项校验方法
前端·vue.js·elementui
NiNg_1_2341 小时前
Echarts连接数据库,实时绘制图表详解
前端·数据库·echarts
如若1232 小时前
对文件内的文件名生成目录,方便查阅
java·前端·python
滚雪球~2 小时前
npm error code ETIMEDOUT
前端·npm·node.js
沙漏无语2 小时前
npm : 无法加载文件 D:\Nodejs\node_global\npm.ps1,因为在此系统上禁止运行脚本
前端·npm·node.js
supermapsupport2 小时前
iClient3D for Cesium在Vue中快速实现场景卷帘
前端·vue.js·3d·cesium·supermap
brrdg_sefg2 小时前
WEB 漏洞 - 文件包含漏洞深度解析
前端·网络·安全
胡西风_foxww3 小时前
【es6复习笔记】rest参数(7)
前端·笔记·es6·参数·rest