rollup技术揭秘系列七 includeStatements(可能是全网最系统性的rollup源码分析文章)

387 阅读5分钟

includeStatements

tree-shaking 本来做的是删除代码的意思。我们需要注意的是 rollup 在删除无用代码之前是先将代码标记为“included” ,最后调用 includeStatements() 将需要保留的代码打包到 chunks 中。接下来我们继续分析 this.includeStatements() 的逻辑。

class Graph {
  //...
  async build(): Promise<void> {
    //...
    //遍历所有的ast.node并且修改node.included的值
    this.includeStatements();
    //...
  }
  private includeStatements(): void {
    for (const module of [...this.entryModules, ...this.implicitEntryModules]) {
          //标记模块 isExecuted = true;
        markModuleAndImpureDependenciesAsExecuted(module);
    }
    if (this.options.treeshake) {
        let treeshakingPass = 1;
        do {
            timeStart(`treeshaking pass ${treeshakingPass}`, 3);
            this.needsTreeshakingPass = false;
            for (const module of this.modules) {
                if (module.isExecuted) {
                    if (module.info.moduleSideEffects === 'no-treeshake') {
                            module.includeAllInBundle();
                    } else {
                            module.include();
                    }
                }
            }
            if (treeshakingPass === 1) {
                // 仅需在第一次的时候将模块内的导出语句包含进来
                for (const module of [...this.entryModules, ...this.implicitEntryModules]) {
                    //module.preserveSignature => 'exports-only'
                    if (module.preserveSignature !== false) {
                        module.includeAllExports(false);
                        this.needsTreeshakingPass = true;
                    }
                }
            }
            timeEnd(`treeshaking pass ${treeshakingPass++}`, 3);
        } while (this.needsTreeshakingPass); //this.needsTreeshakingPass为true的时候才会继续执行treeShaking逻辑
    } else {
        for (const module of this.modules) module.includeAllInBundle();
    }
    //...
    }
}

includeStatements 内部首先会遍历所有入口模块执行 markModuleAndImpureDependenciesAsExecuted(module); 将 module.isExecuted 修改为 true; 接着 if 语句判断是否存在 this.options.treeshake ,是则执行 module.include() 逻辑,否则执行 module.includeAllInBundle()。也就是将所有代码都打包输出。

当然,我们使用 rollup 打包的时候程序帮我们默认开启了 treeshake 。 因此我们看到 if 代码块内部的逻辑,首先定义了 treeshakingPass=1,并且执行过 do 语句之后 treeshakingPass 会执行 ++ 的操作,因此 module.includeAllExports(false) 的逻辑只会执行一次。do...while 语句表示 do 里面的逻辑至少会被执行一次。for (const module of this.modules) 的内部首先判断了 module.isExecuted 并且 module.info.moduleSideEffects !== 'no-treeshake' 才会执行 module.include() 逻辑。因为在执行 markModuleAndImpureDependenciesAsExecuted(module) 的时候就已经将 modulel.isExecuted 设置为 true 了。并且 module.info.moduleSideEffects 默认是一个对象。所以默认情况下会执行 module.include() 逻辑。

module.include 代码定义在 src/Module.ts 中:

class Module {
  //...
  include(): void {
    //context => {"brokenFlow":0,"includedCallArguments":{},"includedLabels":{}}
    const context = createInclusionContext();
    if (this.ast!.shouldBeIncluded(context)) this.ast!.include(context, false);
  }
}

module.include 方法内部首先执行 const context = createInclusionContext() 会得到这么一个对象‘{"brokenFlow":0,"includedCallArguments":{},"includedLabels":{}}’。接着判断 this.ast.shouldBeIncluded(context)为 true 才去执行 this.ast.include(context, false) 方法。this.ast.include 实际上调用的就是 NodeBase.shouldBeIncluded 方法。

NodeBase.shouldBeIncluded

//src/ast/nodes/shared/Node.ts

class NodeBase extends ExpressionEntity implements ExpressionNode {
  //...
  shouldBeIncluded(context: InclusionContext): boolean {
    return this.included || (!context.brokenFlow && this.hasEffects(createHasEffectsContext()));
  }
  //...
}

这里的 shouldBeIncluded 方法内部 this.included 中的 this 就是 Program,默认值为 false。 context.brokenFlow 为 0,this.hasEffects(createHasEffectsContext()) 实际上就是执行 Program.hasEffects 方法,它接收一个由 createHasEffectsContext() 方法创建的一个对象类型的参数。

createHasEffectsContext()

//src/ast/ExecutionContext.ts

export function createHasEffectsContext(): HasEffectsContext {
  return {
    accessed: new PathTracker(), //被访问过的实体会存放在这里
    assigned: new PathTracker(), //被赋值调用过的实体会存放在这里
    brokenFlow: BROKEN_FLOW_NONE, //0
    called: new DiscriminatedPathTracker(), //被调用过的实体会存放到这里
    ignore: {
      breaks: false,
      continues: false,
      labels: new Set(),
      returnYield: false
    },
    includedLabels: new Set(),
    instantiated: new DiscriminatedPathTracker(), //实例
    replacedVariableInits: new Map()
  };
}

Program.hasEffects 内部其实就是遍历 Program.body 对其所有的子节点调用 node.hasEffects(context) 方法来判断是否返回了 true。如果是的话才会调用 this.ast.include(context, false) 将 Program.included 设置为 true。Program.included 为 true 就意味着这个 module 的代码块需要打包到输出的 bundle。

//src/ast/nodes/Program.ts

export default class Program extends NodeBase {
  //...
  hasEffects(context: HasEffectsContext): boolean {
    // 设置 hasCachedEffect 缓存
    if (this.hasCachedEffect) return true;
    for (const node of this.body) {
      //遍历所有子节点进行hasEffects判断,如果返回了true,程序就会将Program.included设置为true。
      if (node.hasEffects(context)) {
        return (this.hasCachedEffect = true);
      }
    }
    return false;
  }
  //...
}

总结

通过本章节的分析,我们知道 Program.shouldBeIncluded 其实取决于它内部所有的子节点是否有任意一个节点调用 hasEffects 返回 true。如果返回了 true 则证明 Program 需要打包到输出到 bundle。但是具体哪些节点需要被包含到最终 bundle 里面则需要进一步判断 node.hasEffects(createHasEffectsContext()) 的返回值了。下一章节我们继续分析 node.hasEffects() 在不同的节点类型时是如何实现的。

rollup 揭秘相关文章