乐趣区

关于前端:揭秘-Rollup-Tree-Shaking

  • 揭秘 Rollup Tree Shaking

    • 前言
    • 比照 webpack
    • 如何应用 rollup
    • 应用 tree shaking 性能
    • rollup 源码解析
    • magic-string
    • acorn

      • AST 工作流
      • AST 解析过程
      • Scope
    • 实现 rollup
    • 实现 tree shaking
    • 依赖的变量有做批改操作
    • 反对块级作用域
    • 解决入口处 tree shaking
    • 实现变量重命名
    • 总结
    • 援用

揭秘 Rollup Tree Shaking

文章首发于 @careteen/rollup,转载请注明起源即可。

前言

rollup 随着 ES2015 公布 Scripts and Modules 横空出世。

Next-generation ES module bundler官网定义此为 下一代 ES 模块捆绑器

能够发现目前前端最火的几个框架 Angular/React/Vue 都在应用 rollup 作为打包工具。

rollup依赖于 ES6 的 module,提供Tree-shaking 性能。对其直译为 树木摇摆,行将树木下面枯死的树叶摇摆下来。对应到编程中则是去除掉无用代码。这对于大型项目能起到肯定优化作用。

比方在做一些中后盾时会应用到 [ant-design]() 局部组件,然而如果间接 import Ant from 'antd' 会将所有组件代码进行打包,而官网也提供了 babel-plugin-import 插件反对按需加载。从肯定水平减小我的项目体积,使页面更快出现给用户。

比照 webpack

  • webpack能够进行 代码分隔 , 动态资源解决 , 热模块替换
  • rollup反对 ES6 moduletree-shaking 功能强大;但 webpack 不反对导出ES6 module
  • webpack打包体积臃肿,rollup打包后简洁,更靠近源代码。

比照两者各自个性,能够发现 webpack 更适宜于 利用 ,而rollup 更实用于 类库

我的项目中都在应用 webpack 进行开发,抱着知其然并知其所以然的态度也学习并实现了一个简版 webpack,并且钻研其热更新原理。感兴趣的同学能够返回浏览。

如何应用 rollup

上面示例代码寄存在 rollup 根底应用,可返回进行调试。

我的项目根目录新建文件 rollup.config.dev.js 做如下配置

// rollup.config.dev.js
import babel from 'rollup-plugin-babel'
import resolve from '@rollup/plugin-node-resolve'
import commonjs from '@rollup/plugin-commonjs'
import typescript from '@rollup/plugin-typescript'
import {terser} from 'rollup-plugin-terser'
import postcss from 'rollup-plugin-postcss'
import serve from 'rollup-plugin-serve'

export default {
  input: './src/index.ts',
  output: {
    file: 'dist/index.js',
    format: 'es',
  },
  plugins: [
    babel({exclude: 'node_modules/**',}),
    resolve(),
    commonjs(),
    typescript(),
    terser(),
    postcss(),
    serve({
      open: true,
      port: 2333,
      contentBase: './dist',
    }),
  ],
}
  • rollup-plugin-babel 反对应用新语法,用 babel 进行编译输入。
  • @rollup/plugin-node-resolve 反对解析第三方模块,即 node_modules 目录下。
  • @rollup/plugin-commonjs 反对commonjs 标准。(因为默认只反对ES6 module
  • @rollup/plugin-typescript 反对解析typescript
  • rollup-plugin-terser 反对压缩js
  • rollup-plugin-postcss 反对编译css
  • rollup-plugin-serve 反对启动本地服务器

再新建 src/index.ts 文件

// src/index.ts
console.log('hello rollup')

package.json 文件中配置

// package.json
"scripts": {"dev": "rollup --config rollup.config.dev.js -w",},

运行脚本npm run dev

应用 tree shaking 性能

新建文件src/userinfo.ts

// src/userinfo.ts
export const name = 'careteen'
export const age = 25

改变src/index.ts

// src/index.ts
import {name, age} from './userinfo'
console.log(name)

运行脚本 npm run dev 后查看dist/index.js

const name = 'careteen'
console.log(name)

可发现,rollup将两个文件合并成一个文件输入,去除了无用代码 export const age = 25,而且也去除了import/export 语句。

rollup 源码解析

当下最新 rollup 功能丰富,架构简单。而此文次要是想钻研 tree-shaking 性能,故翻看 rollup 提交记录,找到 rollup@0.2.0 最后可用版本。
文件数量和代码行数都比拟少,也不便咱们能读懂。

.
├── Bundle
│   └── index.js # 负责打包
├── Module
│   └── index.js # 负责解析模块
├── ast
│   ├── Scope.js # 作用域链
│   ├── analyse.js # 解析 ast 语法树
│   └── walk.js # 遍历 ast 语法树
├── finalisers # 输入类型
│   ├── amd.js
│   ├── cjs.js
│   ├── es6.js
│   ├── index.js
│   └── umd.js
├── rollup.js # 入口
└── utils # 工具函数
    ├── map-helpers.js
    ├── object.js
    ├── promise.js
    └── replaceIdentifiers.js
  • rollup.js打包入口文件
  • Bundle/index.js打包工具,打包时生成一个 Bundle 实例,收集依赖的所有模块,最初将代码打包在一起输入
  • Module/index.js每个文件就是一个模块
  • ast/Scope.js构建作用域和作用域链
  • ast/analyse.js剖析 Ast 作用域和依赖项
  • ast/walk.js遍历Ast

简略浏览代码依赖的第三方库,能够看到 magic-string 和 acorn,咱们先做个简略理解。

magic-string

作用是能够对源代码进行轻微的批改和替换。

const MagicString = require('magic-string')

const s = new MagicString(`export var name = 'careteen'`)
// 返回剪切后的字符
console.log(s.snip(0, 6).toString(), s.toString()) // export, export var name = 'careteen'
// 移除指定地位区间字符
console.log(s.remove(0, 7).toString(), s.toString()) // var name = 'careteen', var name = 'careteen'

// 用指定分隔符拼接源代码
const b = new MagicString.Bundle()
b.addSource({
  content: `var name = 'careteen'`,
  separator: '\n',
})
b.addSource({
  content: `var age = 25`,
  separator: '\n',
})
console.log(b.toString()) // var name = 'careteen' \n var age = 25

acorn

上面波及代码寄存在 @careteen/rollup – prepos,感兴趣可返回调试。

JavaScript解析器,将源代码解析成形象语法树AST

树上定义了代码的构造,通过操作这棵树,能够精准的定位到 申明语句、赋值语句、运算语句 等等。实现对代码的 剖析、优化、变更 等操作。

AST 工作流

  • Parse(解析) 将源代码转换成形象语法树,树上有很多的 estree 节点
  • Transform(转换) 对形象语法树进行转换
  • Generate(代码生成) 将上一步通过转换过的形象语法树生成新的代码

AST 解析过程

借助 astexplorer 能够实时预览源代码的解析后果。并且设置 Parser Settingsacorn

上面代码解析后果如图

import $ from 'jquery';

那如何去遍历形象语法树,并在适合的机会操作它呢?

采纳 深度优先遍历

还不理解的同学返回图解深度优先遍历算法

// walk.js
// DFS
function walk (node, { enter, leave}) {visit(node, null, enter, leave)
}

function visit (node, parent, enter, leave) {if (enter) {enter.call(null, node, parent)
  }
  const keys = Object.keys(node).filter(key => typeof node[key] === 'object')
  keys.forEach(key => {const value = node[key]
    if (Array.isArray(value)) {
      value.forEach(val => {visit(val, node, enter, leave)
      })
    } else if (value && value.type) {visit(value, node, enter, leave)
    }
  })
  if (leave) {leave.call(null, node, parent)
  }
}

module.exports = walk

此逻辑在源码 rollup/ast/walk.js 处

输入形象语法树

// ast.js
const walk = require('./walk')
const acorn = require('acorn')

const ast = acorn.parse(
  `import $ from 'jquery';`,
  {
    locations: true,
    ranges: true,
    sourceType: 'module',
    ecmaVersion: 8,
  }
)

let ident = 0
const padding = () => ' '.repeat(ident)

// Test
ast.body.forEach(statement => {
  walk(statement, {enter(node) {if (node.type) {console.log(padding() + node.type)
        ident += 2
      }
    },
    leave(node) {if (node.type) {
        ident -= 2
        console.log(padding() + node.type)
      }
    }
  })
})

// 输入后果
/**
ImportDeclaration
  ImportDefaultSpecifier
    Identifier
    Identifier
  ImportDefaultSpecifier
  Literal
  Literal
ImportDeclaration
 */

冀望的输入后果和上图构造一样。

Scope

源码处还有此文件 rollup/ast/Scope.js 绝对独立,其实为创立作用域的繁难实现。

// scope.js
class Scope {constructor(options = {}) {
    this.name = options.name
    this.parent = options.parent
    this.names = options.params || []}
  add(name) {this.names.push(name)
  }
  findDefiningScope(name) {if (this.names.includes(name)) {return this}
    if (this.parent) {return this.parent.findDefiningScope(name)
    }
    return null
  }
}

module.exports = Scope

此逻辑在源码 rollup/ast/scope.js 处

看看如何应用

// useScope.js
const Scope = require('./scope')

var a = 1
function one() {
  var b = 2
  function two() {
    var c = 3
    console.log(a, b, c)
  }
  two()}
one()

// 构建 scope chain
const globalScope = new Scope({
  name: 'global',
  parent: null,
})
const oneScope = new Scope({
  name: 'one',
  parent: globalScope,
})
const twoScope = new Scope({
  name: 'two',
  parent: oneScope,
})

globalScope.add('a')
oneScope.add('b')
twoScope.add('c')

console.log(twoScope.findDefiningScope('a'))
console.log(oneScope.findDefiningScope('c'))
console.log(globalScope.findDefiningScope('d'))

// 输入后果
// 1 2 3
// Scope {name: 'global', parent: null, names: [ 'a'] }
// null
// null

此文件次要作用是创立作用域和作用域链,并且将申明的变量挂载到对应的作用域,而且也提供办法 findDefiningScope 查找具体变量所在的作用域。

其意义重大,rollup能够借助他判断变量是否为以后文件定义,否则为 import 导入,进而递归直到找到变量定义所在作用域,而后将依赖写入。

ES module个性是 export 的是值的援用,在 import 后如果对其批改会扭转源,即只是个浅拷贝。

找到变量所在作用域后,可间接将其源代码剪切过去输入。(前面有具体实现)

实现 rollup

新建可调式的配置文件,将 src/index.js 作为入口文件,打包后输入到dest/bundle.js

// ./example/myRollup/rollup.config.js
const path = require('path')
const rollup = require('../../src/rollup')

const entry = path.resolve(__dirname, 'src/index.js')
rollup(entry, 'dest/bundle.js')

入口文件则依赖于 bundle 对其真正编译打包。

// .src/rollup.js
const Bundle = require('./bundle')

function rollup(entry, filename) {
  const bundle = new Bundle({entry,})
  bundle.build(filename)
}

module.exports = rollup

初步思路则是解析入口文件 entryPath 内容,对源代码做解析解决,并输入到指标文件下。

// .src/bundle.js
const path = require('path')

class Bundle {constructor(options) {this.entryPath = path.resolve(options.entry.replace(/\.js$/, '') +'.js')
    this.modules = {}}
  build(filename) {console.log(this.entryPath, filename)
  }
}

module.exports = Bundle

大抵流程为

    1. 获取入口文件的内容,包装成module,生成形象语法树
    1. 对入口文件形象语法树进行依赖解析
    1. 生成最终代码
    1. 写入指标文件

      // .src/bundle.js
      const {readFileSync, writeFileSync} = require('fs')
      const {resolve} = require('path')
      const Module = require('./module')
      const MagicString = require('magic-string')
      
      class Bundle {constructor(options) {this.entryPath = resolve(options.entry.replace(/\.js$/, '') +'.js')
        this.modules = {}
        this.statements = []}
      build(filename) {
        // 1. 获取入口文件的内容,包装成 `module`,生成形象语法树
        const entryModule = this.fetchModule(this.entryPath)
        // 2. 对入口文件形象语法树进行依赖解析
        this.statements = entryModule.expandAllStatements()
        // 3. 生成最终代码
        const {code} = this.generate()
        // 4. 写入指标文件
        writeFileSync(filename, code)
      }
      fetchModule(importee) {
        let route = importee
        if (route) {const code = readFileSync(route, 'utf-8')
       const module = new Module({
         code,
         path: importee,
         bundle: this,
       })
       return module
        }
      }
      generate() {const ms = new MagicString.Bundle()
        this.statements.forEach(statement => {const source = statement._source.clone()
       ms.addSource({
         content: source,
         separator: '\n',
       })
        })
        return {code: ms.toString()
        }
      }
      }
      
      module.exports = Bundle

每一个文件即是一个module,会将源代码解析成形象语法树,而后将源代码挂载到树的节点上,并提供开展批改办法。

// ./src/module.js
const {parse} = require('acorn')
const MagicString = require('magic-string')
const analyse = require('./ast/analyse')

class Module {
  constructor({
    code,
    path,
    bundle,
  }) {
    this.code = new MagicString(code, {filename: path,})
    this.path = path
    this.bundle = bundle
    this.ast = parse(code, {
      ecmaVersion: 7,
      sourceType: 'module',
    })
    this.analyse()}
  analyse() {analyse(this.ast, this.code, this)
  }
  expandAllStatements() {const allStatements = []
    this.ast.body.forEach(statement => {const statements = this.expandStatement(statement)
      allStatements.push(...statements)
    })
    return allStatements
  }
  expandStatement(statement) {
    statement._included = true
    const result = []
    result.push(statement)
    return result
  }
}

module.exports = Module

将源代码挂载到树的节点上。

// ./src/ast/analyse.js
function analyse(ast, ms) {
  ast.body.forEach(statement => {
    Object.defineProperties(statement, {
      _source: {value: ms.snip(statement.start, statement.end)
      }
    })
  })
}

module.exports = analyse

实现 tree shaking

将调试文件内容做如下批改,测试 tree-shaking 性能

// ./example/myRollup/src/index.js
import {name, age} from './userinfo'

function say() {console.log('hi', name)
}

say()

依赖的 userinfo 文件

// ./example/myRollup/src/userinfo.js
export var name = 'careteen'
export var age = 25

冀望打包后果为

var name = 'careteen'
function say() {console.log('hi', name)
}

say()

须要做如下 // + 处新增和批改

// ./src/bundle.js
const {readFileSync, writeFileSync} = require('fs')
const {resolve, isAbsolute, dirname} = require('path') // +
const Module = require('./module')
const MagicString = require('magic-string')

class Bundle {constructor(options) {this.entryPath = resolve(options.entry.replace(/\.js$/, '') +'.js')
    this.modules = {}
    this.statements = []}
  build(filename) {const entryModule = this.fetchModule(this.entryPath)
    this.statements = entryModule.expandAllStatements()
    const {code} = this.generate()
    writeFileSync(filename, code)
  }
  fetchModule(importee, importer) { // +
    let route // +
    if (!importer) { // +
      route = importee // +
    } else { // +
      if (isAbsolute(importee)) { // +
        route = importee // +
      } else if (importee[0] === '.') { // +
        route = resolve(dirname(importer), importee.replace(/\.js$/, '') +'.js') // +
      } // +
    } // +
    if (route) {const code = readFileSync(route, 'utf-8')
      const module = new Module({
        code,
        path: importee,
        bundle: this,
      })
      return module
    }
  }
  generate() {const ms = new MagicString.Bundle()
    this.statements.forEach(statement => {const source = statement._source.clone()
      if (/^Export/.test(statement.type)) { // +
        if (statement.type === 'ExportNamedDeclaration') { // +
          source.remove(statement.start, statement.declaration.start) // +
        } // +
      } // +
      ms.addSource({
        content: source,
        separator: '\n',
      })
    })
    return {code: ms.toString()
    }
  }
}

module.exports = Bundle

次要是批改 fetchModule 办法,解决相似于 import {name} from './userinfo' 的状况时,userinfo中若有定义变量或者依赖其余文件变量时,做递归解决。

生成代码时 generate 办法中过滤掉 ExportNamedDeclaration 语句,而将变量定义间接输入。

- export var name = 'careteen'
+ var name = 'careteen'

对 module 模块做解决

// ./src/module.js
const {parse} = require('acorn')
const MagicString = require('magic-string')
const analyse = require('./ast/analyse')

function hasOwn(obj, prop) { // +
  return Object.prototype.hasOwnProperty.call(obj, prop) // +
} // +

class Module {
  constructor({
    code,
    path,
    bundle,
  }) {
    this.code = new MagicString(code, {filename: path,})
    this.path = path
    this.bundle = bundle
    this.ast = parse(code, {
      ecmaVersion: 7,
      sourceType: 'module',
    })
    // +++ start +++
    this.imports = {} // 导入的变量
    this.exports = {} // 导出的变量
    this.definitions = {} // 变量定义的语句
    // +++ end +++
    this.analyse()}
  analyse() {
    // +++ start +++
    // 收集导入和导出变量
    this.ast.body.forEach(node => {if (node.type === 'ImportDeclaration') {
        const source = node.source.value
        node.specifiers.forEach(specifier => {const { name: localName} = specifier.local
          const {name} = specifier.imported
          this.imports[localName] = {
            source,
            name,
            localName,
          }
        })
      } else if (node.type === 'ExportNamedDeclaration') {const { declaration} = node
        if (declaration.type === 'VariableDeclaration') {const { name} = declaration.declarations[0].id
          this.exports[name] = {
            node,
            localName: name,
            expression: declaration,
          }
        }
      }
    })
    // +++ end +++
    analyse(this.ast, this.code, this)
    // +++ start +++
    // 收集所有语句定义的变量,建设变量和申明语句之间的对应关系
    this.ast.body.forEach(statement => {Object.keys(statement._defines).forEach(name => {this.definitions[name] = statement
      })
    })
    // +++ end +++
  }
  expandAllStatements() {const allStatements = []
    this.ast.body.forEach(statement => {
      // +++ start +++
      // 过滤 `import` 语句
      if (statement.type === 'ImportDeclaration') {return}
      // +++ end +++
      const statements = this.expandStatement(statement)
      allStatements.push(...statements)
    })
    return allStatements
  }
  expandStatement(statement) {
    statement._included = true
    const result = []
    // +++ start +++
    const dependencies = Object.keys(statement._dependsOn)
    dependencies.forEach(name => {const definition = this.define(name)
      result.push(...definition)
    })
    // +++ end +++
    result.push(statement)
    return result
  }
  // +++ start +++
  define(name) {if (hasOwn(this.imports, name)) {const importDeclaration = this.imports[name]
      const mod = this.bundle.fetchModule(importDeclaration.source, this.path)
      const exportDeclaration = mod.exports[importDeclaration.name]
      if (!exportDeclaration) {throw new Error(`Module ${mod.path} does not export ${importDeclaration.name} (imported by ${this.path})`)
      }
      return mod.define(exportDeclaration.localName)
    } else {let statement = this.definitions[name]
      if (statement && !statement._included) {return this.expandStatement(statement)
      } else {return []
      }
    }
  }
  // +++ end +++
}

module.exports = Module

须要做几件事

  • 收集导入和导出变量

    • 建设映射关系,不便后续应用
  • 收集所有语句定义的变量

    • 建设变量和申明语句之间的对应关系,不便后续应用
  • 过滤 import 语句

    • 删除关键词
  • 输入语句时,判断变量是否为import

    • 如是须要递归再次收集依赖文件的变量
    • 否则间接输入
  • 构建依赖关系,创立作用域链,交由 ./src/ast/analyse.js 文件解决

    • 在形象语法树的每一条语句上挂载_source(源代码)、_defines(以后模块定义的变量)、_dependsOn(内部依赖的变量)、_included(是否曾经蕴含在输入语句中)
    • 收集每个语句上定义的变量,创立作用域链
    • 收集内部依赖的变量
// ./src/ast/analyse.js
// +++ start +++
const Scope = require('./scope')
const walk = require('./walk')

function analyse(ast, ms) {let scope = new Scope()
  // 创立作用域链、ast.body.forEach(statement => {function addToScope(declarator) {const { name} = declarator.id
      scope.add(name)
      if (!scope.parent) { // 如果没有下层作用域,阐明是模块内的定级作用域
        statement._defines[name] = true
      }
    }
    Object.defineProperties(statement, {
      _source: { // 源代码
        value: ms.snip(statement.start, statement.end),
      },
      _defines: { // 以后模块定义的变量
        value: {},},
      _dependsOn: { // 以后模块没有定义的变量,即内部依赖的变量
        value: {},},
      _included: { // 是否曾经蕴含在输入语句中
        value: false,
        writable: true,
      },
    })
    // 收集每个语句上定义的变量,创立作用域链
    walk(statement, {enter(node) {
        let newScope
        switch (node.type) {
          case 'FunctionDeclaration':
            const params = node.params.map(p => p.name)
            addToScope(node)
            newScope = new Scope({
              parent: scope,
              params,
            })
            break;
          case 'VariableDeclaration':
            node.declarations.forEach(addToScope)
            break;
        }
        if (newScope) {
          Object.defineProperty(node, '_scope', {value: newScope,})
          scope = newScope
        }
      },
      leave(node) {if (node._scope) {scope = scope.parent}
      },
    })
  })
  ast._scope = scope
  // 收集内部依赖的变量
  ast.body.forEach(statement => {
    walk(statement, {enter(node) {if (node.type === 'Identifier') {const { name} = node
          const definingScope = scope.findDefiningScope(name)
          // 作用域链中找不到 则阐明为内部依赖
          if (!definingScope) {statement._dependsOn[name] = true
          }
        }
      },
    })
  })
}

module.exports = analyse
// +++ end +++

依赖的遍历和作用域可间接应用上文提到的 walk 和 Scope 的实现。

依赖的变量有做批改操作

通过上述解决,曾经根本实现繁难 tree-shaking 性能,然而对于如下 依赖文件中导出变量有做过批改 还需解决

// ./example/myRollup/src/userinfo.js
export var name = 'careteen'
name += 'lan'
name ++
export var age = 25

须要在 ./src/module.js 文件下做如下 // + 的新增批改

// ./src/module.js
class Module {constructor() {
    // ...
    this.definitions = {} // 变量定义的语句
    this.modifications = {} // 批改的变量 // +
    this.analyse()}
  analyse() {
    // ...
    // 收集所有语句定义的变量,建设变量和申明语句之间的对应关系
    this.ast.body.forEach(statement => {Object.keys(statement._defines).forEach(name => {this.definitions[name] = statement
      })
      // +++ start +++
      Object.keys(statement._modifies).forEach(name => {if (!hasOwn(this.modifications, name)) {this.modifications[name] = []}
        // 可能有多处批改
        this.modifications[name].push(statement)
      })
      // +++ end +++
    })    
  }
  expandStatement(statement) {
    statement._included = true
    const result = []
    const dependencies = Object.keys(statement._dependsOn)
    dependencies.forEach(name => {const definition = this.define(name)
      result.push(...definition)
    })
    result.push(statement)
    // +++ start +++
    // 以后模块下所定义的变量 若有批改 则退出 result
    const defines = Object.keys(statement._defines)
    defines.forEach(name => {const modifications = hasOwn(this.modifications, name) && this.modifications[name]
      if (modifications) {
        modifications.forEach(modif => {if (!modif._included) {const statements = this.expandStatement(modif)
            result.push(...statements)
          }
        })
      }
    })
    // +++ end +++
    return result
  }  
}

须要做几件事

  • 在形象语法树每条语句中定义批改的变量 _modifies (交由src/ast/analyse.js 解决)

    • 收集内部依赖的变量(下面曾经实现过)
    • 收集变量批改的语句
  • 将所有批改语句的变量寄存到modifications
  • 输入语句时,判断定义的变量 _defines 中是否含有批改语句变量,若有则须要输入
// ./src/ast/analyse.js
function analyse(ast, ms) {
  ast.body.forEach(statement => {
    // ...
    Object.defineProperties(statement, {
      // ...
      _modifies: { // 批改的变量
        value: {}, // +},
    })    
  })
  ast.body.forEach(statement => {
    // +++ start +++
    // 收集内部依赖的变量
    function checkForReads(node) {if (node.type === 'Identifier') {const { name} = node
        const definingScope = scope.findDefiningScope(name)
        // 作用域链中找不到 则阐明为内部依赖
        if (!definingScope) {statement._dependsOn[name] = true
        }
      }
    }
    // 收集变量批改的语句
    function checkForWrites(node) {function addNode(n) {while (n.type === 'MemberExpression') {// var a = 1; var obj = { c: 3}; a += obj.c;
          n = n.object
        }
        if (n.type !== 'Identifier') {return}
        statement._modifies[n.name] = true
      }
      if (node.type === 'AssignmentExpression') {addNode(node.left)
      } else if (node.type === 'UpdateExpression') { // var a = 1; a++
        addNode(node.argument)
      } else if (node.type === 'CallExpression') {node.arguments.forEach(addNode)
      }
    }
    // // +++ end +++
    walk(statement, {enter(node) {
        // +++ start +++
        if (node._scope) {scope = node._scope}
        checkForReads(node)
        checkForWrites(node)
      },
      leave(node) {if (node._scope) {scope = scope.parent}
      }
      // +++ end +++
    })
  })  
}

通过上述解决后,可失去冀望的输入后果

// ./example/myRollup/dest/bundle.js
var name = 'careteen'
name += 'lan'
name ++
function say() {console.log('hi', name)
}
say()

反对块级作用域

对于如下语句,还需提供反对

if(true) {var blockVariable = 25}
console.log(blockVariable)

须要做如下解决

// ./src/ast/scope.js
class Scope {constructor(options = {}) {
    // ...
    this.isBlockScope = !!options.block // 是否为块作用域
  }
  // +++ start +++
  add(name, isBlockDeclaration) {if (this.isBlockScope && !isBlockDeclaration) { // 以后作用域是块级作用域 && 此语句为 var 或申明函数
      this.parent.add(name, isBlockDeclaration)
    } else {this.names.push(name)
    }
  }
  // +++ end +++
}
  • 创立作用域时,辨别块级作用域和一般变量定义
// ./src/ast/analyse.js
function analyse(ast, ms) {
  ast.body.forEach(statement => {function addToScope(declarator, isBlockDeclaration = false) { // +
      const {name} = declarator.id
      scope.add(name, isBlockDeclaration) // +
      // ...
    }
    // ...
    // 收集每个语句上定义的变量,创立作用域链
    walk(statement, {enter(node) {
        let newScope
        switch (node.type) {
          // +++ start +++
          case 'FunctionExpression':
          case 'FunctionDeclaration':
            const params = node.params.map(p => p.name)
            if (node.type === 'FunctionDeclaration') {addToScope(node)
            } else if (node.type === 'FunctionExpression' && node.id) {params.push(node.id.name)
            }
            newScope = new Scope({
              parent: scope,
              params,
              block: true,
            })
            break;
          case 'BlockStatement':
            newScope = new Scope({
              parent: scope,
              block: true,
            })
            break;
          case 'VariableDeclaration':
            node.declarations.forEach(variableDeclarator => {if (node.kind === 'let' || node.kind === 'const') {addToScope(variableDeclarator, true)
              } else {addToScope(variableDeclarator, false)
              }
            })
            break;
          // +++ end +++
        }
      }
    } 
  }
}

输入语句时将零碎变量 console.log 做解决,防止输入两次。

// ./src/module.js
const SYSTEM_VARIABLE = ['console', 'log']
class Module {
  // ...
  define(name) {if (hasOwn(this.imports, name)) {// ...} else {let statement = this.definitions[name]
      // +++ start +++
      if (statement && !statement._included) {return this.expandStatement(statement)
      } else if (SYSTEM_VARIABLE.includes(name)) {return []
      } else {throw new Error(`variable '${name}' is not exist`)
      }
      // +++ end +++
    }
  }  
}

解决入口处 tree shaking

上述 tree-shaking 是针对于有 import 语句时的解决,对于入口文件有定义但未应用变量时,还需解决

var company = 'sohu focus'
var companyAge = 23
console.log(company)
  • 过滤定义但未应用的变量
  • 收集定义变量时,如果变量曾经输入则不再输入
// ./src/module.js
class Module {
  // ...
  expandAllStatements() {if (statement.type === 'ImportDeclaration') {return}
    // +++ start +++
    // 过滤定义但未应用的变量
    if (statement.type === 'VariableDeclaration') {return}
    // +++ end +++
  }
  define(name) {if (hasOwn(this.imports, name)) {// ...} else {let statement = this.definitions[name]
      // +++ start +++
      if (statement) {if (statement._included) {return []
        } else {return this.expandStatement(statement)
        }
        // +++ end +++
      } else if (SYSTEM_VARIABLE.includes(name)) {return []
      } else {throw new Error(`variable '${name}' is not exist`)
      }
    }
  }  
}

实现变量重命名

存在如下状况,多个模块都有某个变量雷同命名company

// ./example/myRollup/src/compay1.ts
const company = 'qunar'
export const company1 = company + '1'

// ./example/myRollup/src/compay2.ts
const company = 'sohu'
export const company2 = company + '2'
// ./example/myRollup/src/index.ts
import {company1} from './compay1'
import {company2} from './compay2'
console.log(company1, company2)

此时须要在打包时对重名变量适当重命名再输入

先抽离和筹备工具函数

// ./src/utils.js
const walk = require('./ast/walk')

function hasOwn(obj, prop) {return Object.prototype.hasOwnProperty.call(obj, prop)
}

function replaceIdentifiers(statement, source, replacements) {
  walk(statement, {enter(node) {if (node.type === 'Identifier') {if (node.name && replacements[node.name]) {source.overwrite(node.start, node.end, replacements[node.name])
        }
      }
    }
  })
}

module.exports = {
  hasOwn,
  replaceIdentifiers,
}

在形象语法树每条语句上挂载以后模块的实例

// ./src/ast/analyse.js
function analyse(ast, ms, module) { // +
  ast.body.forEach(statement => {
    Object.defineProperties(statement, {
      _module: { // module 实例
        value: module, // +
      },
      // ...
    }
  }
}

在 module 上提供重命名的办法

// ./src/module.js
class Module {constructor() {
    // ...
    this.canonicalNames = {} // 不重名的变量
    this.analyse()}
  // +++ start ++
  rename(name, replacement) {this.canonicalNames[name] = replacement
  }
  getCanonicalName(localName) {if (!hasOwn(this.canonicalNames, localName)) {this.canonicalNames[localName] = localName
    }
    return this.canonicalNames[localName]
  }
  // +++ end ++
}
  • 收集反复命名的变量
  • 重命名反复命名的变量

    // ./src/bundle.js
    const {hasOwn, replaceIdentifiers} = require('./utils')
    
    class Bundle {constructor(options) {build(filename) {const entryModule = this.fetchModule(this.entryPath)
      this.statements = entryModule.expandAllStatements()
      this.definesConflict() // +
      const {code} = this.generate()
      writeFileSync(filename, code)
    }
    // +++ start +++
    definesConflict() {const defines = {}
      const conflicts = {}
      this.statements.forEach(statement => {Object.keys(statement._defines).forEach(name => {if (hasOwn(defines, name)) {conflicts[name] = true
          } else {defines[name] = []}
          defines[name].push(statement._module)
        })
      })
      Object.keys(conflicts).forEach(name => {const modules = defines[name]
        modules.pop() // 最初一个重名的不解决
        modules.forEach(module => {const replacement = getSafeName(name)
          module.rename(name,replacement)
        })
      })
      function getSafeName(name) {while (hasOwn(conflicts, name)) {name = `_${name}`
        }
        conflicts[name] = true
        return name
      }
    }
    // +++ end +++
    generate() {const ms = new MagicString.Bundle()
      this.statements.forEach(statement => {
        // +++ start +++
        let replacements = {}
        Object.keys(statement._dependsOn)
          .concat(Object.keys(statement._defines))
          .forEach(name => {const canonicalName = statement._module.getCanonicalName(name)
            if (name !== canonicalName) {replacements[name] = canonicalName
            }
          })
        // +++ end +++
        const source = statement._source.clone()
        if (/^Export/.test(statement.type)) {if (statement.type === 'ExportNamedDeclaration') {source.remove(statement.start, statement.declaration.start)
          }
        }
        replaceIdentifiers(statement, source, replacements) // +
      }
    }
    }

进过上述解决,可失去如下输入后果

// ./example/myRollup/dest/bundle.js
const _company = 'qunar'
const company1 = _company + '1'
const company = 'sohu'
const company2 = company + '2'
console.log(company1, company2)

✌🏻 😁 ✌🏻

总结

本文从 rollup 应用再到源码揭秘,实现了 Tree-shaking 繁难性能,所有代码寄存在 @careteen/rollup 中。感兴趣的同学能够返回调试。

援用

  • Rollup 官网
  • ECMA Module
  • ES module 工作原理
  • webpack 繁难实现
  • commonjs 标准原理
  • 在线解析 AST
退出移动版