Webpack 之 basic chunk graph

2,693 阅读10分钟

源文件以及配置文件

有以下文件 a.js / b.js / c.js / d.js 以及 webpack.config.js, 其中 a.js 为入口文件,它们之间的依赖关系如下图,实心箭头代表异步加载。

// a.js - 入口文件
import add from './b.js'
add(1, 2)
import('./c.js').then(del => del(1, 2))
  
// b.js
import mod from './d.js'
export default function add(n1, n2) {
  return n1 + n2
}
mod(100, 11)

// c.js
import mod from './d.js'
mod(100, 11)
import('./b.js').then(add => add(1, 2))
export default function del(n1, n2) {
  return n1 - n2
}

// d.js
export default function mod(n1, n2) {
  return n1 % n2
}

// webpack.js
module.exports = {
  entry: {
    app: './src/a.js'
  },
  output: {
    filename: '[name].[chunkhash].js',
    chunkFilename: '[name].bundle.[chunkhash:8].js',
    publicPath: '/'
  },
  optimization: {
    runtimeChunk: {
      name: 'bundle'
    }
  },
}

概念

chunkGroup:一个 chunkGroup 可以包含多个 chunk,可以通过 chunks 字段看出它由哪些 chunk 组成。Webpack 会为每个入口创建一个 entrypoint,下图就是入口 app: './src/a.js' 的 entrypoint,可以看出 entrypoint 就是一个 chunkGroup。

chunk:一个 chunk 可以包含多个 module,可以通过 _groups 字段看出它所属的 chunkGroup,通过 _modules 看出它由哪些 module 组成。Webpack 在为每个入口创建 entrypoint 的同时,也会创建一个 chunk,如下图:

module:资源文件,如 js / css / 图片 等,可以通过 _chunks 看出它所属的 chunk,通过 blocks 看出它异步加载的模块。Webpack 会将它封装成 NormalModule 对象,'./src/a.js' 对应的 NormalModule 对象如下图:

block:在模块中异步加载的模块,比如:import('./c.js').then(),Webpack 会将它封装成 ImportDependenciesBlock。在 a.js 中异步加载的 c.js 封装之后的结构如下:

简写说明

为了在后续的源码解析中更清晰的描述,使用如下简写。另外:本次源码解析着重于 module graph & basic chunk graph 的创建,多余的分支不讲,因为我也没看。

  • NM('./src/a.js'):'./src/a.js' 模块文件封装之后的 NormalModule.

  • chunk('app'):Webpack 为入口创建的 chunk.

  • chunkGroup('app'):Webpack 为入口创建的 chunkGroup.

  • block('./c.js'):异步加载的模块 './c.js' 封装之后的 ImportDependenciesBlock.

Webpack 会为每个异步加载的模块创建一个 chunk & chunkGroup,并关联它们。

  • chunk('./c.js'):为异步加载的模块 './c.js' 创建的 chunk.

  • chunkGroup('./c.js'):为异步加载的模块 './c.js' 创建的 chunkGroup.

创建并关联入口 chunk & chunkGroup

上一次讲模块对象的时候,讲了 Compiler 对象的3个钩子:run, compilation, make,当 make 结束后,会调用 compilation.seal(),开始触发 Compilation 对象的钩子。

// /webpack/lib/Compiler.js
class Compiler extends Tapable {
  run(callback) {
    ...
    this.hooks.beforeRun.callAsync(this, err => {
      this.hooks.run.callAsync(this, err => {
        ...
        this.compile(onCompiled);
      });
    });
  },
  compile(callback) {
    const params = this.newCompilationParams();
    this.hooks.beforeCompile.callAsync(params, err => {
      if (err) return callback(err);		
      this.hooks.compile.call(params);
      const compilation = this.newCompilation(params);
      // 触发 make 钩子
      this.hooks.make.callAsync(compilation, err => {
        if (err) return callback(err);

        compilation.finish();

        compilation.seal(err => {
          if (err) return callback(err);

          this.hooks.afterCompile.callAsync(compilation, err => {
            if (err) return callback(err);

            return callback(null, compilation);
          });
        });
      });
    });
  }
}

// /webpack/lib/compilation.js
class Compilation {
  ...
  seal () {
    ...
    // 创建 chunk 之前的 hook
    this.hooks.beforeChunks.call();
    // 根据 addEntry 方法中收集到入口文件组成的 _preparedEntrypoints 数组
    for (const preparedEntrypoint of this._preparedEntrypoints) {
      const module = preparedEntrypoint.module; // 即 NM('./src/a.js')
      const name = preparedEntrypoint.name; // 即 'app'
      const chunk = this.addChunk(name); // 为每个入口创建 chunk
      const entrypoint = new Entrypoint(name); // 为每个入口创建 entrypoint,它就是 chunkGroup
      entrypoint.setRuntimeChunk(chunk); // 设置为 runtime chunk
      entrypoint.addOrigin(null, name, preparedEntrypoint.request);
      this.namedChunkGroups.set(name, entrypoint);
      this.entrypoints.set(name, entrypoint);
      this.chunkGroups.push(entrypoint);
        
      // 建立 chunkGroup 和 chunk 之间的关系
      GraphHelpers.connectChunkGroupAndChunk(entrypoint, chunk);
      // 建立 chunk 和 module 之间的关系
      GraphHelpers.connectChunkAndModule(chunk, module);
        
      chunk.entryModule = module;
      chunk.name = name;
        
      this.assignDepth(module);
    }
    this.processDependenciesBlocksForChunkGroups(this.chunkGroups.slice()); //接下来的重点
    // 对 module 进行排序
    this.sortModules(this.modules);
    // 创建 chunk 之后的 hook
    this.hooks.afterChunks.call(this.chunks);
    this.hooks.optimize.call();

    while (
      this.hooks.optimizeModulesBasic.call(this.modules) ||
  	  this.hooks.optimizeModules.call(this.modules) ||
	  this.hooks.optimizeModulesAdvanced.call(this.modules)
    ) {
      /* empty */
    }
    // 优化 module 之后的 hook
    this.hooks.afterOptimizeModules.call(this.modules);
    while (
      this.hooks.optimizeChunksBasic.call(this.chunks, this.chunkGroups) ||
	  this.hooks.optimizeChunks.call(this.chunks, this.chunkGroups) ||
	  // 主要涉及到 webpack.config.js optimization 配置
 	  this.hooks.optimizeChunksAdvanced.call(this.chunks, this.chunkGroups)
    ) {
      /* empty */
    }
    // 优化 chunk 之后的 hook
    this.hooks.afterOptimizeChunks.call(this.chunks, this.chunkGroups);
    ...
  }
  ...
}

// GraphHelpers.js
/**
 * @param {ChunkGroup} chunkGroup the ChunkGroup to connect
 * @param {Chunk} chunk chunk to tie to ChunkGroup
 * @returns {void}
 */
GraphHelpers.connectChunkGroupAndChunk = (chunkGroup, chunk) => {
  if (chunkGroup.pushChunk(chunk)) {
    chunk.addGroup(chunkGroup);
  }
};
/**
 * @param {Chunk} chunk Chunk to connect to Module
 * @param {Module} module Module to connect to Chunk
 * @returns {void}
 */
GraphHelpers.connectChunkAndModule = (chunk, module) => {
  if (module.addChunk(chunk)) {
    chunk.addModule(module);
  }
};

Webpack 会遍历配置文件中的入口,为每个入口创建一个 chunk & chunkGroup,此时的 chunk 还没收集任何 NormalModule,包括入口文件对应的 NormalModule。

将 chunk 设为 runtimeChunk,当 Webpack 编译完成后,webpack runtime 代码会注入到 runtimeChunk。

调用 GraphHelpers.connectChunkGroupAndChunk() GraphHelpers.connectChunkAndModule() 建立 chunk & chunkGroup 之间的关系,以及 chunk & 入口模块 之间的关系。这里还未涉及 chunk 和 入口模块依赖的模块的关系。

至此,Webpack 已经创建了 chunkGroup('app') & chunk('app'),chunk('app').modules = Set(NM('./src/a.js'))

processDependenciesBlocksForChunkGroups

接下来我们重点看一下 this.processDependenciesBlocksForChunkGroups(this.chunkGroups.slice());

该函数主要实现了两块功能:

  1. 创建 module graph,保存在 blockInfoMap.
  2. 根据 module graph 创建 basic chunk graph,即每个 chunk 关联 modules.

创建 module graph

const iteratorDependency = d => {
  // We skip Dependencies without Reference
  const ref = this.getDependencyReference(currentModule, d);
  if (!ref) {
    return;
  }
  // We skip Dependencies without Module pointer
  const refModule = ref.module;
  if (!refModule) {
    return;
  }
  // We skip weak Dependencies
  if (ref.weak) {
    return;
  }

  blockInfoModules.add(refModule);
};

const iteratorBlockPrepare = b => {
  blockInfoBlocks.push(b);
  // blockQueue push b(异步依赖),从而进入到下一次的内层循环
  blockQueue.push(b);
};

// 本次 compilation 包含的所有 module
for (const modules of this.modules) {
  blockQueue = [module];
  currentModule = module;
  while (blockQueue.length > 0) {
    block = blockQueue.pop(); // 同步 module / 异步 block
    blockInfoModules = new Set(); // 保存模块依赖的同步 module
    blockInfoBlocks = []; // 保存模块依赖的异步 block

    if (block.variables) {
      iterationBlockVariable(block.variables, iteratorDependency);
    }

    // 在 blockInfoModules 中添加 dependencies 中的普通 module
    if (block.dependencies) {
      iterationOfArrayCallback(block.dependencies, iteratorDependency);
    }

    // 在 blockInfoBlocks 和 blockQueue 数组中添加异步 module,
    // 这样异步 block 也会进入到内层循环,去获取异步 block 的依赖
    if (block.blocks) {
      iterationOfArrayCallback(block.blocks, iteratorBlockPrepare);
    }

    const blockInfo = {
      modules: Array.from(blockInfoModules),
      blocks: blockInfoBlocks
    };
    // blockInfoMap 上保存了每个 module 依赖的同步 modules 及 异步 blocks
    blockInfoMap.set(block, blockInfo);
  }
}
  • this.modules = [NM('./src/a.js'), NM('./b.js'), NM('./c.js'), NM('./d.js')]

  • 第一次外层循环:blockQueue = [NM('./src/a.js')], currentModule = NM('./src/a.js')

    • 第一次内层循环 - blockQueue pop NM('./src/a.js'),开始处理 NM('./src/a.js')。先忽略 if (block.variables),进入 if (block.dependencies) ,遍历 NM('./src/a.js').dependencies 执行 iteratorDependency,在 blockInfoModules 中添加 NM('./src/a.js') 的同步依赖;然后进入 if (block.blocks) ,遍历 NM('./src/a.js').blocks 执行 iteratorBlockPrepare,在 blockInfoBlocks 中添加 NM('./src/a.js') 的异步依赖,并且 blockQueue push 该异步依赖。此时 blockQueue & blockInfoMap 如下:

      blockQueue = [Block('./c.js')];
      blockInfoMap = Map({
        key: NM('./src/a.js'),
        value: { modules: [NM('./b.js')], blocks: [Block('./c.js')] }
      })
      
    • 第二次内层循环 - blockQueue pop Block('./c.js'),开始处理 Block('./c.js'),步骤同上,此时 blockQueue & blockInfoMap 如下,内层循环结束。

      blockQueue = [];
      blockInfoMap = Map({
        key: NM('./src/a.js'),
        value: { modules: [NM('./b.js')], blocks: [Block('./c.js')] }
      }, {
        key: Block('./c.js'),
        value: { modules: [NM('./c.js')], blocks: [] }
      })
      
  • 第二次外层循环:blockQueue = [NM('./b.js')], currentModule = NM('./b.js')

    • 第一次内层循环 - blockQueue pop NM('./b.js'),开始处理 NM('./b.js'),步骤同上,此时 blockQueue & blockInfoMap 如下:

      blockQueue = [];
      blockInfoMap = Map({
        key: NM('./src/a.js'),
        value: { modules: [NM('./b.js')], blocks: [Block('./c.js')] }
      }, {
        key: Block('./c.js'),
        value: { modules: [NM('./c.js')], blocks: [] }
      }, {
        key: NM('./b.js'),
        value: { modules: [NM('./d.js')], blocks: [] }
      })
      
  • 第三次外层循环:blockQueue = [NM('./c.js')], currentModule = NM('./c.js')

    • 第一次内层循环 - blockQueue pop NM('./c.js'),开始处理 NM('./c.js'),步骤同上,此时 blockQueue & blockInfoMap 如下:

      blockQueue = [Block('./b.js')];
      blockInfoMap = Map({
        key: NM('./src/a.js'),
        value: { modules: [NM('./b.js')], blocks: [Block('./c.js')] }
      }, {
        key: Block('./c.js'),
        value: { modules: [NM('./c.js')], blocks: [] }
      }, {
        key: NM('./b.js'),
        value: { modules: [NM('./d.js')], blocks: [] }
      }, {
        key: NM('./c.js'),
        value: { modules: [NM('./d.js')], blocks: [Block('./b.js')] }
      })
      
    • 第二次内层循环 - blockQueue pop Block('./b.js'),开始处理 Block('./b.js'),步骤同上,此时 blockQueue & blockInfoMap 如下:

      blockQueue = [];
      blockInfoMap = Map({
        key: NM('./src/a.js'),
        value: { modules: [NM('./b.js')], blocks: [Block('./c.js')] }
      }, {
        key: Block('./c.js'),
        value: { modules: [NM('./c.js')], blocks: [] }
      }, {
        key: NM('./b.js'),
        value: { modules: [NM('./d.js')], blocks: [] }
      }, {
        key: NM('./c.js'),
        value: { modules: [NM('./d.js')], blocks: [Block('./b.js')] }
      }, {
        key: Block('./b.js'),
        value: { modules: [NM('./b.js')], blocks: [] }
      })
      
  • 第四次外层循环:blockQueue = [NM('./d.js')], currentModule = NM('./d.js')

    • 第一次内层循环 - blockQueue pop NM('./d.js'),开始处理 NM('./d.js'),步骤同上,此时 blockQueue & blockInfoMap 如下:

      blockQueue = [];
      blockInfoMap = Map({
        key: NM('./src/a.js'),
        value: { modules: [NM('./b.js')], blocks: [Block('./c.js')] }
      }, {
        key: Block('./c.js'),
        value: { modules: [NM('./c.js')], blocks: [] }
      }, {
        key: NM('./b.js'),
        value: { modules: [NM('./d.js')], blocks: [] }
      }, {
        key: NM('./c.js'),
        value: { modules: [NM('./d.js')], blocks: [Block('./b.js')] }
      }, {
        key: Block('./b.js'),
        value: { modules: [NM('./b.js')], blocks: [] }
      }, {
        key: NM('./d.js'),
        value: { modules: [NM('./d.js'), blocks: [] }
      })
      

至此,我们已经把本次 compilation 中所有模块的同步 & 异步依赖信息保存在 blockInfoMap 中。

创建 chunk graph

// For each async Block in graph
/**
 * @param {AsyncDependenciesBlock} b iterating over each Async DepBlock
 * @returns {void}
 */
const iteratorBlock = b => {
  // 1. We create a chunk for this Block
  // but only once (blockChunkGroups map)
  let c = blockChunkGroups.get(b);
  if (c === undefined) {
    c = this.namedChunkGroups.get(b.chunkName);
    if (c && c.isInitial()) {
      this.errors.push(
        new AsyncDependencyToInitialChunkError(b.chunkName, module, b.loc)
      );
      c = chunkGroup;
    } else {
      // 通过 addChunkInGroup 创建新的 chunkGroup 及 chunk,并返回 chunkGroup
      c = this.addChunkInGroup(
        b.groupOptions || b.chunkName,
        module, // 这个 block 所属的 module
        b.loc,
        b.request
      );
      chunkGroupCounters.set(c, { index: 0, index2: 0 });
      blockChunkGroups.set(b, c);
      allCreatedChunkGroups.add(c);
    }
  } else {
    // TODO webpack 5 remove addOptions check
    if (c.addOptions) c.addOptions(b.groupOptions);
    c.addOrigin(module, b.loc, b.request);
  }

  // 2. We store the Block+Chunk mapping as dependency for the chunk
  let deps = chunkDependencies.get(chunkGroup);
  if (!deps) chunkDependencies.set(chunkGroup, (deps = []));
  // 当前 chunkGroup 所依赖的 block 及 chunkGroup
  deps.push({
    block: b,
    chunkGroup: c,
    couldBeFiltered: true
  });
  // 异步的 block 使用创建的新的 chunkGroup
  // 3. We enqueue the DependenciesBlock for traversal
  queueDelayed.push({
    action: PROCESS_BLOCK,
    block: b,
    module: module,
    chunk: c.chunks[0], // 获取新创建的 chunkGroup 中的第一个 chunk,即 block 需要被加入的 chunk
    chunkGroup: c // 异步 block 使用新创建的 chunkGroup
  });
};
...
const ADD_AND_ENTER_MODULE = 0;
const ENTER_MODULE = 1;
const PROCESS_BLOCK = 2;
const LEAVE_MODULE = 3;
...
const chunkGroupToQueueItem = chunkGroup => ({
  action: ENTER_MODULE,
  block: chunkGroup.chunks[0].entryModule,
  module: chunkGroup.chunks[0].entryModule,
  chunk: chunkGroup.chunks[0],
  chunkGroup
});

let queue = inputChunkGroups.map(chunkGroupToQueueItem).reverse()

while (queue.length) {
  while (queue.length) {
    const queueItem = queue.pop();
    module = queueItem.module;
    block = queueItem.block;
    chunk = queueItem.chunk;
    chunkGroup = queueItem.chunkGroup;

    switch (queueItem.action) {
      case ADD_AND_ENTER_MODULE: {
        // We connect Module and Chunk when not already done
        if (chunk.addModule(module)) {
          module.addChunk(chunk);
        } else {
          // already connected, skip it
          break;
        }
      }
      // fallthrough
      case ENTER_MODULE: {
        ...
        queue.push({
          action: LEAVE_MODULE,
          block,
          module,
          chunk,
          chunkGroup
        });
      }
      // fallthrough
      case PROCESS_BLOCK: {
        // get prepared block info
        const blockInfo = blockInfoMap.get(block);
        // Traverse all referenced modules
        for (let i = blockInfo.modules.length - 1; i >= 0; i--) {
          const refModule = blockInfo.modules[i];
          if (chunk.containsModule(refModule)) {
            // skip early if already connected
            continue;
          }
          // enqueue the add and enter to enter in the correct order
          // this is relevant with circular dependencies
          queue.push({
            action: ADD_AND_ENTER_MODULE,
            block: refModule, // 依赖 module
            module: refModule, // 依赖 module
            chunk, // module 所属的 chunk
            chunkGroup // module 所属的 chunkGroup
          });
        }

        // Traverse all Blocks
        iterationOfArrayCallback(blockInfo.blocks, iteratorBlock);

        if (blockInfo.blocks.length > 0 && module !== block) {
          blocksWithNestedBlocks.add(block);
        }
        break;
      }
      case LEAVE_MODULE: {
        ...
        break;
      }
    }
  }
  const tempQueue = queue;
  queue = queueDelayed.reverse();
  queueDelayed = tempQueue;
}
  • 初始化 queue = [$1]

    $1: {
      action: ENTER_MODULE(1),
      block: NM('./src/a.js'),
      module: NM('./src/a.js'),
      chunk: { name: 'app', _groups: Set(Entrypoint), _modules: Set(NM('./src/a.js'))},
      chunkGroup: Entrypoint{ chunks: [chunk('app')] }
    }
    
  • 第一次外层循环:

    • 第一次内层循环 - queue pop $1,开始处理 chunk('app'),进入到 case ENTER_MODULE:,忽略其他代码,直接看 queue.push({}),push 如下对象:

      $2: {
        action: LEAVE_MODULE(3),
        block: NM('./src/a.js'),
        module: NM('./src/a.js'),
        chunk: { name: 'app', _groups: Set(Entrypoint), _modules: Set(NM('./src/a.js'))},
        chunkGroup: Entrypoint{ chunks: [chunk('app')] }
      }
      

      注意 case ENTER_MODULE: 没有 break,所以会接着进入 case PROCESS_BLOCK:,上面得到的 blockInfoMap 在这用到了,首先遍历 NM('./src/a.js') 依赖的 modules - [NM('./b.js')],如果 chunk('app') 还未关联,就 push queue 如下对象:

      $3: {
        action: ADD_AND_ENTER_MODULE(0),
        block: NM('./b.js'),
        module: NM('./b.js'),
        chunk: { name: 'app', _groups: Set(Entrypoint), _modules: Set(NM('./src/a.js'))},
        chunkGroup: Entrypoint{ chunks: [chunk('app')] }
      }
      

      接着遍历 NM('./src/a.js') 依赖的 blocks - [Block('./c.js')],针对每一个 block,创建并关联 chunkGroup & chunk,然后 queueDelayed.push 以下对象,queueDelayed 用于在第二次外层循环之前重新初始化 queue,后面会用到。至此第一次内层循环结束,此时 queue = [$2, $3], chunk('app').modules = Set(NM('./src/a.js'))

      {
        action: PROCESS_BLOCK(2),
        block: Block('./c.js'),
        module: NM('./src/a.js'),
        chunk: { _groups: Set(ChunkGroup), _modules: Set(0)},
        chunkGroup: ChunkGroup{ chunks: [chunk('./c.js')]}
      }
      
    • 第二次内层循环 - queue pop $3,进入到 case ADD_AND_ENTER_MODULE:,关联 chunk('app') & NM('./b.js'),接着进入 case ENTER_MODULE:,忽略其他代码,直接看 queue.push({}),push 如下对象:

      $4: {
        action: LEAVE_MODULE(3),
        block: NM('./b.js'),
        module: NM('./b.js'),
        chunk: { name: 'app', _groups: Set(Entrypoint), _modules: Set(NM('./src/a.js'), NM('./b.js'))},
        chunkGroup: Entrypoint{ chunks: [chunk('app')] }
      }
      

      接着进入 case PROCESS_BLOCK:,遍历 NM('./b.js') 依赖的 modules - [NM('./d.js')],如果 chunk('app') 还未关联, 就 push queue 如下对象.

      $5: {
        action: ADD_AND_ENTER_MODULE(0),
        block: NM('./d.js'),
        module: NM('./d.js'),
        chunk: { name: 'app', _groups: Set(Entrypoint), _modules: Set(NM('./src/a.js'), NM('./b.js'))},
        chunkGroup: Entrypoint{ chunks: [chunk('app')] }
      }
      

      由于 NM('./b.js') 没有依赖的 blocks,所以不用 push queueDelayed,至此第二次内层循环结束,queue = [$2, $4, $5], chunk('app').modules = Set(NM('./src/a.js'), NM('./b.js'))

    • 第三次内层循环 - queue pop $5,进入到 case ADD_AND_ENTER_MODULE:,关联 chunk('app') & NM('./d.js'),接着进入 case ENTER_MODULE:,忽略其他代码,直接看 queue.push({}),push 如下对象:

      $6: {
        action: LEAVE_MODULE(3),
        block: NM('./d.js'),
        module: NM('./d.js'),
        chunk: { name: 'app', _groups: Set(Entrypoint), _modules: Set(NM('./src/a.js'), NM('./b.js'), NM('./d.js'))},
        chunkGroup: Entrypoint{ chunks: [chunk('app')] }
      }
      

      遍历 NM('./d.js') 依赖的 modules & blocks,由于两者都为空,所以没有 queue.push,至此第三次内层循环结束,queue = [$2, $4, $6], chunk('app').modules = Set(NM('./src/a.js'), NM('./b.js'), NM('./d.js'))

      接下来的三次内层循环都是进入 case LEAVE_MODULE:,先不管里面的逻辑。至此内层循环结束,此时 queue = [], chunk('app').modules = Set(NM('./src/a.js'), NM('./b.js'), NM('./d.js'))

  • 使用上次外层循环中赋值的 queueDelayed 重新初始化 queue,queue = [$1],并将 queueDelayed 置空,开始关联第二个 chunk & modules。

    $1: {
      action: PROCESS_BLOCK(2),
      block: Block('./c.js'),
      module: NM('./src/a.js'),
      chunk: { _groups: Set(ChunkGroup), _modules: Set(0)},
      chunkGroup: ChunkGroup{ chunks: [chunk('./c.js')]}
    }
    
  • 第二次外层循环:

    • 第一次内层循环 - queue pop $1,进入 case PROCESS_BLOCK: ,遍历 Block('./c.js') 依赖的 modules - [NM('./d.js')],如果 chunk('./c.js') 还未关联,就 push queue 如下对象:

      $2: {
        action: ADD_AND_ENTER_MODULE(0),
        block: NM('./c.js'),
        module: NM('./c.js'),
        chunk: { _groups: Set(ChunkGroup), _modules: Set(0)},
        chunkGroup: ChunkGroup{ chunks: [chunk('./c.js')]}
      }
      

      由于 Block('./c.js') 没有依赖的 blocks,所以不用 push queueDelayed,至此第一次内层循环结束,queue: [$2], chunk('./c.js').modules = Set(0)

    • 第二次内层循环 - queue pop $2,进入 case ADD_AND_ENTER_MODULE:,关联 chunk('./c.js') & NM('./c.js'),接着进入 case ENTER_MODULE:,忽略其他代码,直接看 queue.push({}),push 如下对象:

      $3: {
        action: LEAVE_MODULE(3),
        block: NM('./c.js'),
        module: NM('./c.js'),
        chunk: { _groups: Set(ChunkGroup), _modules: Set(NM('./c.js'))},
        chunkGroup: ChunkGroup{ chunks: [chunk('./c.js')] }
      }
      

      接着进入 case PROCESS_BLOCK:,遍历 NM('./c.js') 依赖的 modules - [NM('./d.js')],如果 chunk('./c.js') 还未关联,就 push queue 如下对象:

      $4: {
        action: ADD_AND_ENTER_MODULE(0),
        block: NM('./d.js'),
        module: NM('./d.js'),
        chunk: { _groups: Set(ChunkGroup), _modules: Set(NM('./c.js'))},
        chunkGroup: ChunkGroup{ chunks: [chunk('./c.js')] }
      }
      

      接着遍历 NM('./c.js') 依赖的 blocks - [Block('./b.js')],针对每一个 block,创建并关联 chunkGroup & chunk; 然后 queueDelayed.push 以下对象,至此第二次内层循环结束,此时 queue = [$3, $4], chunk('./c.js').modules = Set(NM('./c.js'))

      {
        action: PROCESS_BLOCK(2),
        block: Block('./b.js'),
        module: NM('./c.js'),
        chunk: { _groups: Set(ChunkGroup), _modules: Set(0)},
        chunkGroup: ChunkGroup{ chunks: [chunk('./b.js')]}
      }
      
    • 第三次内层循环 - queue pop $4,进入 case ADD_AND_ENTER_MODULE:,关联 chunk('./c.js') & NM('./d.js'),接着进入 case ENTER_MODULE:,忽略其他代码,直接看 queue.push({}),push 如下对象:

      $6: {
        action: LEAVE_MODULE(3),
        block: NM('./d.js'),
        module: NM('./d.js'),
        chunk: { _groups: Set(ChunkGroup), _modules: Set(NM('./c.js'), NM('./d.js'))},
        chunkGroup: ChunkGroup{ chunks: [chunk('./c.js')] }
      }
      

      进入 case PROCESS_BLOCK:,NM('./d.js') 没有依赖的 modules & blocks,至此第三次内层循环结束,此时 queue = [$3, $6], chunk('./c.js').modules = Set(NM('./c.js'), NM('./d.js'))。 接下来的两次内层循环都是进入 case LEAVE_MODULE:,先不管里面的逻辑。至此内层循环结束,此时 queue = [], chunk('./c.js').modules = Set(NM('./c.js'), NM('./d.js'))

  • 使用上次外层循环中赋值的 queueDelayed 重新初始化 queue,queue = [$1],并将 queueDelayed 置空,开始关联第三个 chunk & modules。

    $1: {
      action: PROCESS_BLOCK(2),
      block: Block('./b.js'),
      module: NM('./c.js'),
      chunk: { _groups: Set(ChunkGroup), _modules: Set(0)},
      chunkGroup: ChunkGroup{ chunks: [chunk('./b.js')]}
    }
    
  • 第三次外层循环:

    • 第一次内层循环 - queue pop $1,进入 case PROCESS_BLOCK: ,遍历 Block('./b.js') 依赖的 modules - [NM('./b.js')],如果 chunk('./b.js') 还未关联,就 push queue 如下对象:

      $2: {
        action: ADD_AND_ENTER_MODULE(0),
        block: NM('./b.js'),
        module: NM('./b.js'),
        chunk: { _groups: Set(ChunkGroup), _modules: Set(0)},
        chunkGroup: ChunkGroup{ chunks: [chunk('./b.js')]}
      }
      

      由于 Block('./b.js') 没有依赖的 blocks,所以不用 push queueDelayed,至此第一次内层循环结束,queue: [$2], chunk('./b.js').modules = Set(0)

    • 第二次内层循环 - queue pop $2,进入 case ADD_AND_ENTER_MODULE:,关联 chunk('./b.js') & NM('./b.js'),接着进入 case ENTER_MODULE:,忽略其他代码,直接看 queue.push({}),push 如下对象:

      $3: {
        action: LEAVE_MODULE(3),
        block: NM('./b.js'),
        module: NM('./b.js'),
        chunk: { _groups: Set(ChunkGroup), _modules: Set(NM('./b.js'))},
        chunkGroup: ChunkGroup{ chunks: [chunk('./b.js')] }
      }
      

      接着进入 case PROCESS_BLOCK:,遍历 NM('./b.js') 依赖的 modules - [NM('./d.js')],如果 chunk('./b.js') 还未关联,就 push queue 如下对象:

      $4: {
        action: ADD_AND_ENTER_MODULE(0),
        block: NM('./d.js'),
        module: NM('./d.js'),
        chunk: { _groups: Set(ChunkGroup), _modules: Set(NM('./b.js'))},
        chunkGroup: ChunkGroup{ chunks: [chunk('./b.js')] }
      }
      

      由于 Block('./b.js') 没有依赖的 blocks,所以不用 push queueDelayed,至此第二次内层循环结束,queue: [3,3, 4], chunk('./b.js').modules = Set(NM('./b.js'))。

    • 第三次内层循环 - queue pop $4,进入 case ADD_AND_ENTER_MODULE:,关联 chunk('./b.js') & NM('./d.js'),接着进入 case ENTER_MODULE:,忽略其他代码,直接看 queue.push({}),push 如下对象:

      $5: {
        action: LEAVE_MODULE(3),
        block: NM('./d.js'),
        module: NM('./d.js'),
        chunk: { _groups: Set(ChunkGroup), _modules: Set(NM('./b.js'), NM('./d.js'))},
        chunkGroup: ChunkGroup{ chunks: [chunk('./b.js')] }
      }
      

      接着进入 case PROCESS_BLOCK:,NM('./d.js') 没有依赖的 modules & blocks,至此第三次内层循环结束,此时 queue = [$3, $5], chunk('./b.js').modules = Set(NM('./b.js'), NM('./d.js'))。 接下来的两次内层循环都是进入 case LEAVE_MODULE:,先不管里面的逻辑。至此内层循环结束,此时 queue = [], chunk('./b.js').modules = Set(NM('./b.js'), NM('./d.js'))

  • 至此循环结束,我们得到了三个 chunkGroup,每个 chunkGroup 关联的 chunk 分别是 chunk('app') / chunk('./c.js') / chunk('./b.js'),它们关联的 modules 如下:

    chunk('app').modules = Set(NM('./src/a.js'), NM('./b.js'), NM('./d.js'))
    chunk('./c.js').modules = Set(NM('./c.js'), NM('./d.js'))
    chunk('./b.js').modules = Set(NM('./b.js'), NM('./d.js'))
    

可以看到 NM('./d.js') 在三个 chunk 中都存在,难道最终生成的每个 bundle 中都会打包 NM('./d.js') 么?当然不会,后续 webppack 还会优化 basic chunk graph,生成 chunk graph.

如何 debug Webpack 源码

  • 在 package.json 中添加一条 script

    "debug": "node --inspect-brk ./node_modules/webpack/bin/webpack.js"

  • 终端运行 npm run debug,在浏览器地址栏输入 chrome://inspect/#devices,界面如下:

  • 点击 Open dedicated DevTools for Node,会打开 chrome 调试工具,开始调试 Webpack

参考

webpack系列之六chunk图生成