Hi FE !
Ai
git
前端面试题
前端小tip
  • vite
  • webpack
npm
  • vue2
  • vue3
react
GitHub
Ai
git
前端面试题
前端小tip
  • vite
  • webpack
npm
  • vue2
  • vue3
react
GitHub
  • rollup中的tree-shaking

rollup中的tree-shaking

tree-shaking

在rollup中, tree-shaking的本质是通过代码的静态分析, 分析模块代码中的导入(import), 变量声明(definition), 导出(export) 并在其模块的导入导出上下文中逐层查找, 如果有依赖关系, 则移除export声明(bundle的时候), 如果没有, 则移除掉此语句块. 以此来达到tree-shaking的目的 那么相应的, 我们也需要优化丰富我们之前的代码内容 首先, 我们需要在模块中对模块的导入导出和变量声明进行收集

// 在constructor中预先定义这三个对象
this.imports = {} // 导入的变量
this.exports = {} // 导出的变量
this.definitions = {} // 变量定义的语句

// 在analyze 中通过ast的遍历进行收集
analyse() {
    // 收集导入和导出变量
    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,
          }
        }
      }
    })
    analyse(this.ast, this.code, this)
    // 收集所有语句定义的变量,建立变量和声明语句之间的对应关系
    this.ast.body.forEach(statement => {
      Object.keys(statement._defines).forEach(name => {
        this.definitions[name] = statement
      })
    })
  }

其次, 我们需要在expandAllStatements调用的时候, 同步将其模块依赖进行递归处理, 以使其生成树状的依赖关系 其中expandAllStatement遍历调用了expandStatement, 用于处理每个ast节点 在expandStatement中遍历了_dependsOn(也就是其依赖的变量列表), 遍历变量列表 如果依赖的变量在导入变量中, 则根据变量内容来查找import语句中的导入变量, 然后根据import语句的导入源找到源模块, 通过源模块找到了其导出的变量, 否则从当前作用域中的definitions中找到这个声明, 如果声明之前没有_included(已经包含在输出语句中), 则将继续调用expandStatement

 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)
   return result
 }
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 []
    }
  }
}

这些下划线的变量其实是在analyse步骤中进行处理和分析的 analyse定义了四个内置下划线变量 _source: 代表当前区块的源代码 _defines: 代表当前模块定义的变量 _dependsOn: 代表当前模块没有定义的变量(外部依赖的变量) _included: 是否已经包含在输出语句中, 包含在其中的才会被输出

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

自此, 我们就能够成功实现: 对模块逐一进行分析, 获知到模块的import进来的内容, export出去的内容, 在当前模块上下文中定义了的变量, 当前模块中使用了的变量, 在当前模块中修改了的变量, 等 根据scope进行逐层查找, 以此来获知当前模块中使用的变量在import语句中是否已经存在, 存在则将该语句标记为_dependsOn, 也就是已经被依赖的内容 分析完成依赖关系之后, 会进行打包操作, 将import语句中的内容直接注入替换掉该import语句, 同时移除掉export语句 如果上层变量与下层变量有所冲突, 则需要在后续合并的时候进行重命名 这样就完成了rollup的基础打包工作

rollup与webpack不同

处理维度不同

webpack的核心在于将模块塞到模块数组中用于进行调度, webpack处理的核心维度是"模块" rollup核心处理的是"语句", rollup会将模块的上下文语句进行逐一分析处理, 将会从入口开始, 打包所有的模块, 并清除掉输入输出语句

流程设计不同

rollup的核心在执行流上, 只提供了有限的hooks流程来支持扩展 webpack则是完全设计在可扩展性上的架构, 以至于核心的compile和compilation都是继承自Tapable的, 但这样也会造成代码可读性极差, 阅读源码中的一大堆的回调与tap流程让人摸不着头脑

Edit this page
最近更新: 2025/12/2 01:46
Contributors: qdleader
qdleader
本站总访问量 129823次 | 本站访客数 12人