來源
tree-shaking 最早由 Rich Harris 在 rollup 中提出。
為了減少最終構(gòu)建體積而誕生。
以下是 MDN 中的說明:
tree-shaking 是一個(gè)通常用于描述移除 JavaScript 上下文中的未引用代碼(dead-code) 行為的術(shù)語。
它依賴于 ES2015 中的 import 和 export 語句,用來檢測(cè)代碼模塊是否被導(dǎo)出、導(dǎo)入,且被 JavaScript 文件使用。
在現(xiàn)代 JavaScript 應(yīng)用程序中,我們使用模塊打包(如 webpack 或 Rollup)將多個(gè) JavaScript 文件打包為單個(gè)文件時(shí)自動(dòng)刪除未引用的代碼。這對(duì)于準(zhǔn)備預(yù)備發(fā)布代碼的工作非常重要,這樣可以使最終文件具有簡(jiǎn)潔的結(jié)構(gòu)和最小化大小。
tree-shaking VS dead code elimination
說起 tree-shaking 不得不說起 dead code elimination,簡(jiǎn)稱 DCE。
很多人往往把 tree-shaking 當(dāng)作是一種實(shí)現(xiàn) DCE 的技術(shù)。如果都是同一種東西,最終的目標(biāo)是一致的(更少的代碼)。為什么要重新起一個(gè)名字叫做 tree-shaking 呢?
tree-shaking 術(shù)語的發(fā)明者 Rich Harris 在他寫的一篇《tree-shaking versus dead code elimination》告訴了我們答案。
Rich Harris 引用了一個(gè)做蛋糕的例子。原文如下:
Bad analogy time: imagine that you made cakes by throwing whole eggs into the mixing bowl and smashing them up, instead of cracking them open and pouring the contents out. Once the cake comes out of the oven, you remove the fragments of eggshell, except that’s quite tricky so most of the eggshell gets left in there.
You’d probably eat less cake, for one thing.
That’s what dead code elimination consists of — taking the finished product, and imperfectly removing bits you don’t want. tree-shaking, on the other hand, asks the opposite question: given that I want to make a cake, which bits of what ingredients do I need to include in the mixing bowl?
Rather than excluding dead code, we’re including live code. Ideally the end result would be the same, but because of the limitations of static analysis in JavaScript that’s not the case. Live code inclusion gets better results, and is prima facie a more logical approach to the problem of preventing our users from downloading unused code.
簡(jiǎn)單來說:DCE 好比做蛋糕時(shí),直接放入整個(gè)雞蛋,做完時(shí)再從蛋糕中取出蛋殼。而 tree-shaking 則是先取出蛋殼,在進(jìn)行做蛋糕。兩者結(jié)果相同,但是過程是完全不同的。
dead code
dead code 一般具有以下幾個(gè)特征:
- 代碼不會(huì)被執(zhí)行,不可到達(dá)
- 代碼執(zhí)行的結(jié)果不會(huì)被用到
- 代碼只會(huì)影響死變量(只寫不讀)
使用 webpack 在 mode: development 模式下對(duì)以下代碼進(jìn)行打包:
function app() {
var test = '我是app';
function set() {
return 1;
}
return test;
test = '無法執(zhí)行';
return test;
}
export default app;
最終打包結(jié)果:
eval(
"function app() {\n var test = '我是app';\n function set() {\n return 1;\n }\n return test;\n test = '無法執(zhí)行';\n return test;\n}\n\napp();\n\n\n//# sourceURL=webpack://webpack/./src/main.js?"
);
可以看到打包的結(jié)果內(nèi),還是存在無法執(zhí)行到的代碼塊。
webpack 不支持 dead code elimination 嗎?是的,webpack 不支持。
原來,在 webpack 中實(shí)現(xiàn) dead code elimination 功能并不是 webpack 本身, 而是大名鼎鼎的 uglify。
通過閱讀源碼發(fā)現(xiàn),在 mode: development 模式下,不會(huì)加載 terser-webpack-plugin 插件。
// lib/config/defaults.js
D(optimization, 'minimize', production);
A(optimization, 'minimizer', () => [
{
apply: (compiler) => {
// Lazy load the Terser plugin
const TerserPlugin = require('terser-webpack-plugin');
new TerserPlugin({
terserOptions: {
compress: {
passes: 2
}
}
}).apply(compiler);
}
}
]);
// lib/WebpackOptionsApply.js
if (options.optimization.minimize) {
for (const minimizer of options.optimization.minimizer) {
if (typeof minimizer === 'function') {
minimizer.call(compiler, compiler);
} else if (minimizer !== '...') {
minimizer.apply(compiler);
}
}
}
而 terser-webpack-plugin 插件內(nèi)部使用了 uglify 實(shí)現(xiàn)的。
我們?cè)?mode: production 模式下進(jìn)行打包。
// 格式化后結(jié)果
(() => {
var r = {
225: (r) => {
r.exports = '我是app';
}
},
// ...
})();
可以看到最終的結(jié)果,已經(jīng)刪除了不可執(zhí)行部分的代碼。除此之外,還幫我們壓縮了代碼,刪除了注釋等功能。
tree shaking 無效
tree shaking 本質(zhì)上是通過分析靜態(tài)的 ES 模塊,來剔除未使用代碼的。
_ESModule__ 的特點(diǎn)_只能作為模塊頂層的語句出現(xiàn),不能出現(xiàn)在 function 里面或是 if 里面。(ECMA-262 15.2)
import 的模塊名只能是字符串常量。(ECMA-262 15.2.2)
不管 import 的語句出現(xiàn)的位置在哪里,在模塊初始化的時(shí)候所有的 import 都必須已經(jīng)導(dǎo)入完成。(ECMA-262 15.2.1.16.4 - 8.a)
import binding 是 immutable 的,類似 const。比如說你不能 import { a } from ‘./a’ 然后給 a 賦值個(gè)其他什么東西。(ECMA-262 15.2.1.16.4 - 12.c.3)
—–引用自尤雨溪
我們來看看 tree shaking 的功效。
我們有一個(gè)模塊
// ./src/app.js
export const firstName = 'firstName'
export function getName ( x ) {
return x.a
}
getName({ a: 123 })
export function app ( x ) {
return x * x * x;
}
export default app;
底下是 7 個(gè)實(shí)例。
// 1*********************************************
// import App from './app'
// export function main() {
// var test = '我是index';
// return test;
// }
// console.log(main)
// 2*********************************************
// import App from './app'
// export function main() {
// var test = '我是index';
// console.log(App(1))
// return test;
// }
// console.log(main)
// 3*********************************************
// import App from './app'
// export function main() {
// var test = '我是index';
// App.square(1)
// return test;
// }
// console.log(main)
// 4*********************************************
// import App from './app'
// export function main() {
// var test = '我是index';
// let methodName = 'square'
// App[methodName](1)
// return test;
// }
// console.log(main)
// 6*********************************************
// import * as App from './app'
// export function main() {
// var test = '我是index';
// App.square(1)
// return test;
// }
// console.log(main)
// 7*********************************************
// import * as App from './app'
// export function main() {
// var test = '我是index';
// let methodName = 'square'
// App[methodName](1)
// return test;
// }
// console.log(main)
使用 最簡(jiǎn)單的webpack配置進(jìn)行打包
// webpack.config.js
module.exports = {
entry: './src/index.js',
output: {
filename: 'dist.js'
},
mode: 'production'
};
通過結(jié)果可以看到,前 6 中的打包結(jié)果,都對(duì)死代碼進(jìn)行了消除,只有第 7 種,消除失敗。
/* ... */
const r = 'firstName';
function o(e) {
return e.a;
}
function n(e) {
return e * e * e;
}
o({ a: 123 });
const a = n;
console.log(function () {
return t.square(1), '我是index';
});
本人沒有詳細(xì)了解過,只能猜測(cè)下,由于 JavaScript 動(dòng)態(tài)語言的特性使得靜態(tài)分析比較困難,目前的的解析器是通過靜態(tài)解析的,還無法分析全量導(dǎo)入,動(dòng)態(tài)使用的語法。
對(duì)于更多 tree shaking 執(zhí)行相關(guān)的可以參考一下鏈接:
當(dāng)然了,機(jī)智的程序員是不會(huì)被這個(gè)給難住的,既然靜態(tài)分析不行,那就由開發(fā)者手動(dòng)來將文件標(biāo)記為無副作用(side-effect-free)。
tree shaking 和 sideEffects
sideEffects 支持兩種寫法,一種是 false,另一種是數(shù)組
- 如果所有代碼都不包含副作用,我們就可以簡(jiǎn)單地將該屬性標(biāo)記為
false - 如果你的代碼確實(shí)有一些副作用,可以改為提供一個(gè)數(shù)組
可以在 package.js 中進(jìn)行設(shè)置。
// boolean
{
"sideEffects": false
}
// array
{
"sideEffects": ["./src/app.js", "*.css"]
}
也可以在 module.rules 中進(jìn)行設(shè)置。
module.exports = {
module: {
rules: [
{
test: /\.jsx?$/,
exclude: /(node_modules)/,
use: {
loader: 'babel-loader',
},
sideEffects: false || []
}
]
},
}
設(shè)置了 sideEffects: false,后在重新打包
var e = {
225: (e, r, t) => {
(e = t.hmd(e)).exports = '我是main';
}
},
只剩下 main.js 模塊的代碼,已經(jīng)把 app.js 的代碼消除了。
usedExports
webpack 中除了 sideEffects 還提供了一種另一種標(biāo)記消除的方式。那就是通過配置項(xiàng) usedExports 。
由 optimization.usedExports 收集的信息會(huì)被其它優(yōu)化手段或者代碼生成使用,比如未使用的導(dǎo)出內(nèi)容不會(huì)被生成,當(dāng)所有的使用都適配,導(dǎo)出名稱會(huì)被處理做單個(gè)標(biāo)記字符。 在壓縮工具中的無用代碼清除會(huì)受益于該選項(xiàng),而且能夠去除未使用的導(dǎo)出內(nèi)容。
mode: productions 下是默認(rèn)開啟的。
module.exports = {
//...
optimization: {
usedExports: true,
},
};
usedExports 會(huì)使用 terser 判斷代碼有沒有 sideEffect,如果沒有用到,又沒有 sideEffect 的話,就會(huì)在打包時(shí)替它標(biāo)記上 unused harmony。
最后由 Terser、UglifyJS 等 DCE 工具“搖”掉這部分無效代碼。
tree shaking 實(shí)現(xiàn)原理
tree shaking 本身也是采用靜態(tài)分析的方法。
程序靜態(tài)分析(Static Code Analysis)是指在不運(yùn)行代碼的方式下,通過詞法分析、語法分析、控制流分析、數(shù)據(jù)流分析等技術(shù)對(duì)程序代碼進(jìn)行掃描,驗(yàn)證代碼是否滿足規(guī)范性、安全性、可靠性、可維護(hù)性等指標(biāo)的一種代碼分析技術(shù)
tree shaking 使用的前提是模塊必須采用ES6Module語法,因?yàn)?code>tree Shaking 依賴 ES6 的語法:import 和 export。
接下來我們來看看遠(yuǎn)古版本的 rollup 是怎么實(shí)現(xiàn) tree shaking 的。
- 根據(jù)入口模塊內(nèi)容初始化
Module,并使用acorn進(jìn)行ast轉(zhuǎn)化 - 分析
ast。 尋找import和export關(guān)鍵字,建立依賴關(guān)系 - 分析
ast,收集當(dāng)前模塊存在的函數(shù)、變量等信息 - 再一次分析 ast, 收集各函數(shù)變量的使用情況,因?yàn)槲覀兪歉鶕?jù)依賴關(guān)系進(jìn)行收集代碼,如果函數(shù)變量未被使用,
- 根據(jù)收集到的函數(shù)變量標(biāo)識(shí)符等信息,進(jìn)行判斷,如果是
import,則進(jìn)行Module的創(chuàng)建,重新走上幾步。否則的話,把對(duì)應(yīng)的代碼信息存放到一個(gè)統(tǒng)一的result中。 - 根據(jù)最終的結(jié)果生成
bundle。

源碼版本:v0.3.1
通過 entry 入口文件進(jìn)行創(chuàng)建 bundle,執(zhí)行 build 方法,開始進(jìn)行打包。
export function rollup ( entry, options = {} ) {
const bundle = new Bundle({
entry,
resolvePath: options.resolvePath
});
return bundle.build().then( () => {
return {
generate: options => bundle.generate( options ),
write: ( dest, options = {} ) => {
let { code, map } = bundle.generate({
dest,
format: options.format,
globalName: options.globalName
});
code += `\n//# ${SOURCEMAPPING_URL}=${basename( dest )}.map`;
return Promise.all([
writeFile( dest, code ),
writeFile( dest + '.map', map.toString() )
]);
}
};
});
}
build 內(nèi)部執(zhí)行 fetchModule 方法,根據(jù)文件名,readFile 讀取文件內(nèi)容,創(chuàng)建 Module。
build () {
return this.fetchModule( this.entryPath, null )
.then( entryModule => {
this.entryModule = entryModule;
if ( entryModule.exports.default ) {
let defaultExportName = makeLegalIdentifier( basename( this.entryPath ).slice( 0, -extname( this.entryPath ).length ) );
while ( entryModule.ast._scope.contains( defaultExportName ) ) {
defaultExportName = `_${defaultExportName}`;
}
entryModule.suggestName( 'default', defaultExportName );
}
return entryModule.expandAllStatements( true );
})
.then( statements => {
this.statements = statements;
this.deconflict();
});
}
fetchModule ( importee, importer ) {
return Promise.resolve( importer === null ? importee : this.resolvePath( importee, importer ) )
.then( path => {
/*
緩存處理
*/
this.modulePromises[ path ] = readFile( path, { encoding: 'utf-8' })
.then( code => {
const module = new Module({
path,
code,
bundle: this
});
return module;
});
return this.modulePromises[ path ];
});
}
根據(jù)讀取到的文件內(nèi)容,使用 acorn 編譯器進(jìn)行進(jìn)行 ast 的轉(zhuǎn)化。
//
export default class Module {
constructor ({ path, code, bundle }) {
/*
初始化
*/
this.ast = parse(code, {
ecmaVersion: 6,
sourceType: 'module',
onComment: (block, text, start, end) =>
this.comments.push({ block, text, start, end })
});
this.analyse();
}

遍歷節(jié)點(diǎn)信息。尋找 import 和 export 關(guān)鍵字,這一步就是我們常說的根據(jù) esm 的靜態(tài)結(jié)構(gòu)進(jìn)行分析。
把 import 的信息,收集到 this.imports 對(duì)象中,把 exports 的信息,收集到 this.exports 中.
this.ast.body.forEach( node => {
let source;
if ( node.type === 'ImportDeclaration' ) {
source = node.source.value;
node.specifiers.forEach( specifier => {
const isDefault = specifier.type === 'ImportDefaultSpecifier';
const isNamespace = specifier.type === 'ImportNamespaceSpecifier';
const localName = specifier.local.name;
const name = isDefault ? 'default' : isNamespace ? '*' : specifier.imported.name;
if ( has( this.imports, localName ) ) {
const err = new Error( `Duplicated import '${localName}'` );
err.file = this.path;
err.loc = getLocation( this.code.original, specifier.start );
throw err;
}
this.imports[ localName ] = {
source, // 模塊id
name,
localName
};
});
}
else if ( /^Export/.test( node.type ) ) {
if ( node.type === 'ExportDefaultDeclaration' ) {
const isDeclaration = /Declaration$/.test( node.declaration.type );
this.exports.default = {
node,
name: 'default',
localName: isDeclaration ? node.declaration.id.name : 'default',
isDeclaration
};
}
else if ( node.type === 'ExportNamedDeclaration' ) {
// export { foo } from './foo';
source = node.source && node.source.value;
if ( node.specifiers.length ) {
node.specifiers.forEach( specifier => {
const localName = specifier.local.name;
const exportedName = specifier.exported.name;
this.exports[ exportedName ] = {
localName,
exportedName
};
if ( source ) {
this.imports[ localName ] = {
source,
localName,
name: exportedName
};
}
});
}
else {
let declaration = node.declaration;
let name;
if ( declaration.type === 'VariableDeclaration' ) {
name = declaration.declarations[0].id.name;
} else {
name = declaration.id.name;
}
this.exports[ name ] = {
node,
localName: name,
expression: declaration
};
}
}
}
}

analyse () {
// imports and exports, indexed by ID
this.imports = {};
this.exports = {};
// 遍歷 ast 查找對(duì)應(yīng)的 import、export 關(guān)聯(lián)
this.ast.body.forEach( node => {
let source;
// import foo from './foo';
// import { bar } from './bar';
if ( node.type === 'ImportDeclaration' ) {
source = node.source.value;
node.specifiers.forEach( specifier => {
const isDefault = specifier.type === 'ImportDefaultSpecifier';
const isNamespace = specifier.type === 'ImportNamespaceSpecifier';
const localName = specifier.local.name;
const name = isDefault ? 'default' : isNamespace ? '*' : specifier.imported.name;
if ( has( this.imports, localName ) ) {
const err = new Error( `Duplicated import '${localName}'` );
err.file = this.path;
err.loc = getLocation( this.code.original, specifier.start );
throw err;
}
this.imports[ localName ] = {
source, // 模塊id
name,
localName
};
});
}
else if ( /^Export/.test( node.type ) ) {
// export default function foo () {}
// export default foo;
// export default 42;
if ( node.type === 'ExportDefaultDeclaration' ) {
const isDeclaration = /Declaration$/.test( node.declaration.type );
this.exports.default = {
node,
name: 'default',
localName: isDeclaration ? node.declaration.id.name : 'default',
isDeclaration
};
}
// export { foo, bar, baz }
// export var foo = 42;
// export function foo () {}
else if ( node.type === 'ExportNamedDeclaration' ) {
// export { foo } from './foo';
source = node.source && node.source.value;
if ( node.specifiers.length ) {
// export { foo, bar, baz }
node.specifiers.forEach( specifier => {
const localName = specifier.local.name;
const exportedName = specifier.exported.name;
this.exports[ exportedName ] = {
localName,
exportedName
};
if ( source ) {
this.imports[ localName ] = {
source,
localName,
name: exportedName
};
}
});
}
else {
let declaration = node.declaration;
let name;
if ( declaration.type === 'VariableDeclaration' ) {
name = declaration.declarations[0].id.name;
} else {
name = declaration.id.name;
}
this.exports[ name ] = {
node,
localName: name,
expression: declaration
};
}
}
}
}
// 查找函數(shù),變量,類,塊級(jí)作用與等,并根據(jù)引用關(guān)系進(jìn)行關(guān)聯(lián)
analyse( this.ast, this.code, this );
}
接下來查找函數(shù),變量,類,塊級(jí)作用與等,并根據(jù)引用關(guān)系進(jìn)行關(guān)聯(lián)。
使用 magicString 為每一個(gè) statement 節(jié)點(diǎn)增加內(nèi)容修改的功能。
遍歷整顆 ast 樹,先初始化一個(gè) Scope,作為當(dāng)前模塊的命名空間。如果是函數(shù)或塊級(jí)作用域等則新建一個(gè) Scope。各 Scope 之間通過 parent 進(jìn)行關(guān)聯(lián),建立起一個(gè)根據(jù)命名空間關(guān)系樹。
如果是變量和函數(shù),則與當(dāng)前的 Scope 進(jìn)行關(guān)聯(lián), 把對(duì)應(yīng)的標(biāo)識(shí)符名稱增加到 Scope 的中。到這一步,已經(jīng)收集到了各節(jié)點(diǎn)上出現(xiàn)的函數(shù)和變量。

接下來,再一次遍歷 ast。查找變量函數(shù),是否只是被讀取過,或者只是修改過。
根據(jù) Identifier 類型查找標(biāo)識(shí)符,如果當(dāng)前標(biāo)識(shí)符能在 Scope 中找到,說明有對(duì)其進(jìn)行過讀取。存放在 _dependsOn 集合中。

接下來根據(jù) AssignmentExpression、UpdateExpression 和 CallExpression 類型節(jié)點(diǎn),收集我們的標(biāo)識(shí)符,有沒有被修改過或被當(dāng)前參數(shù)傳遞過。并將結(jié)果存放在 _modifies 中。
function analyse(ast, magicString, module) {
var scope = new Scope();
var currentTopLevelStatement = undefined;
function addToScope(declarator) {
var name = declarator.id.name;
scope.add(name, false);
if (!scope.parent) {
currentTopLevelStatement._defines[name] = true;
}
}
function addToBlockScope(declarator) {
var name = declarator.id.name;
scope.add(name, true);
if (!scope.parent) {
currentTopLevelStatement._defines[name] = true;
}
}
// first we need to generate comprehensive scope info
var previousStatement = null;
var commentIndex = 0;
ast.body.forEach(function (statement) {
currentTopLevelStatement = statement; // so we can attach scoping info
Object.defineProperties(statement, {
_defines: { value: {} },
_modifies: { value: {} },
_dependsOn: { value: {} },
_included: { value: false, writable: true },
_module: { value: module },
_source: { value: magicString.snip(statement.start, statement.end) }, // TODO don't use snip, it's a waste of memory
_margin: { value: [0, 0] },
_leadingComments: { value: [] },
_trailingComment: { value: null, writable: true } });
var trailing = !!previousStatement;
// attach leading comment
do {
var comment = module.comments[commentIndex];
if (!comment || comment.end > statement.start) break;
// attach any trailing comment to the previous statement
if (trailing && !/\n/.test(magicString.slice(previousStatement.end, comment.start))) {
previousStatement._trailingComment = comment;
}
// then attach leading comments to this statement
else {
statement._leadingComments.push(comment);
}
commentIndex += 1;
trailing = false;
} while (module.comments[commentIndex]);
// determine margin
var previousEnd = previousStatement ? (previousStatement._trailingComment || previousStatement).end : 0;
var start = (statement._leadingComments[0] || statement).start;
var gap = magicString.original.slice(previousEnd, start);
var margin = gap.split('\n').length;
if (previousStatement) previousStatement._margin[1] = margin;
statement._margin[0] = margin;
walk(statement, {
enter: function (node) {
var newScope = undefined;
magicString.addSourcemapLocation(node.start);
switch (node.type) {
case 'FunctionExpression':
case 'FunctionDeclaration':
case 'ArrowFunctionExpression':
var names = node.params.map(getName);
if (node.type === 'FunctionDeclaration') {
addToScope(node);
} else if (node.type === 'FunctionExpression' && node.id) {
names.push(node.id.name);
}
newScope = new Scope({
parent: scope,
params: names, // TODO rest params?
block: false
});
break;
case 'BlockStatement':
newScope = new Scope({
parent: scope,
block: true
});
break;
case 'CatchClause':
newScope = new Scope({
parent: scope,
params: [node.param.name],
block: true
});
break;
case 'VariableDeclaration':
node.declarations.forEach(node.kind === 'let' ? addToBlockScope : addToScope); // TODO const?
break;
case 'ClassDeclaration':
addToScope(node);
break;
}
if (newScope) {
Object.defineProperty(node, '_scope', { value: newScope });
scope = newScope;
}
},
leave: function (node) {
if (node === currentTopLevelStatement) {
currentTopLevelStatement = null;
}
if (node._scope) {
scope = scope.parent;
}
}
});
previousStatement = statement;
});
// then, we need to find which top-level dependencies this statement has,
// and which it potentially modifies
ast.body.forEach(function (statement) {
function checkForReads(node, parent) {
if (node.type === 'Identifier') {
// disregard the `bar` in `foo.bar` - these appear as Identifier nodes
if (parent.type === 'MemberExpression' && node !== parent.object) {
return;
}
// disregard the `bar` in { bar: foo }
if (parent.type === 'Property' && node !== parent.value) {
return;
}
var definingScope = scope.findDefiningScope(node.name);
if ((!definingScope || definingScope.depth === 0) && !statement._defines[node.name]) {
statement._dependsOn[node.name] = true;
}
}
}
function checkForWrites(node) {
function addNode(node, disallowImportReassignments) {
while (node.type === 'MemberExpression') {
node = node.object;
}
// disallow assignments/updates to imported bindings and namespaces
if (disallowImportReassignments && has(module.imports, node.name) && !scope.contains(node.name)) {
var err = new Error('Illegal reassignment to import \'' + node.name + '\'');
err.file = module.path;
err.loc = getLocation(module.code.toString(), node.start);
throw err;
}
if (node.type !== 'Identifier') {
return;
}
statement._modifies[node.name] = true;
}
if (node.type === 'AssignmentExpression') {
addNode(node.left, true);
} else if (node.type === 'UpdateExpression') {
addNode(node.argument, true);
} else if (node.type === 'CallExpression') {
node.arguments.forEach(function (arg) {
return addNode(arg, false);
});
}
// TODO UpdateExpressions, method calls?
}
walk(statement, {
enter: function (node, parent) {
// skip imports
if (/^Import/.test(node.type)) return this.skip();
if (node._scope) scope = node._scope;
checkForReads(node, parent);
checkForWrites(node, parent);
//if ( node.type === 'ReturnStatement')
},
leave: function (node) {
if (node._scope) scope = scope.parent;
}
});
});
ast._scope = scope;
}
執(zhí)行完結(jié)果如下:

在上一步種,我們?yōu)楹瘮?shù),變量,類,塊級(jí)作用與等聲明與我們當(dāng)前節(jié)點(diǎn)進(jìn)行了關(guān)聯(lián),現(xiàn)在要把節(jié)點(diǎn)上的這些信息,統(tǒng)一收集起來,放到 Module 中
//
this.ast.body.forEach( statement => {
Object.keys( statement._defines ).forEach( name => {
this.definitions[ name ] = statement;
});
Object.keys( statement._modifies ).forEach( name => {
if ( !has( this.modifications, name ) ) {
this.modifications[ name ] = [];
}
this.modifications[ name ].push( statement );
});
});

從中我們可以看到每個(gè) statement 中,依賴了哪些,修改了哪些。
當(dāng)我們?cè)谌肟谀K的操作完成后,在遍歷 statement 節(jié)點(diǎn),根據(jù) _dependsOn 的中的信息,執(zhí)行 define 。
如果 _dependsOn 的數(shù)據(jù),在 this.imports 中,能夠找到,說明該標(biāo)識(shí)符是一個(gè)導(dǎo)入模塊,調(diào)用 fetchModule 方法,重復(fù)上面的邏輯。
如果是正常函數(shù)變量之類的,則收集對(duì)應(yīng) statement 。執(zhí)行到最后,我們就可以把相關(guān)聯(lián)的 statement 都收集起來,未被收集到,說明其就是無用代碼,已經(jīng)被過濾了。
最后在重組成 bundle,通過 fs 在發(fā)送到我們的文件。
留在最后
tree shaking 還要很多點(diǎn)值得挖掘,如:
- css 的 tree shaking
- webpack 的 tree shaking 實(shí)現(xiàn)
- 如何避免 tree shaking 無效
- ...