Fancy Front End Fancy Front End
  • 开始上手
  • 基础
  • 调度器(Scheduler)
  • 更新器(Updater)
  • 渲染器(Render)
  • 更新周期
  • hooks 原理
  • 总结
  • 📙 React源码漂流记
  • 开始上手
  • 基础
  • reactivity
  • runtime-core
  • runtime-dom
  • Awesome Web
  • Awesome NodeJS
话题
  • 导航
  • Q&A
  • 幻灯片
  • 关于
  • 分类
  • 标签
  • 归档
博客 (opens new window)
GitHub (opens new window)

Jonsam NG

让有意义的事变得有意思,让有意思的事变得有意义
  • 开始上手
  • 基础
  • 调度器(Scheduler)
  • 更新器(Updater)
  • 渲染器(Render)
  • 更新周期
  • hooks 原理
  • 总结
  • 📙 React源码漂流记
  • 开始上手
  • 基础
  • reactivity
  • runtime-core
  • runtime-dom
  • Awesome Web
  • Awesome NodeJS
话题
  • 导航
  • Q&A
  • 幻灯片
  • 关于
  • 分类
  • 标签
  • 归档
博客 (opens new window)
GitHub (opens new window)
  • 开始上手
  • Plan 计划
  • typescript-utility

  • single-spa源码

  • qiankun源码

  • webpack

    • 开始阅读
    • tapable源码

    • init阶段

    • make阶段

      • 本章概要
      • make 阶段:compilation
      • make 阶段:module
        • 目录
        • _handleModuleBuildAndDependencies
        • buildModule
        • build
        • JavascriptParser
        • 参考
      • make 阶段:walk
    • 总结

  • axios

  • solid

  • vite源码

  • jquery源码

  • snabbdom

  • am-editor

  • html2canvas

  • express

  • acorn源码

  • immutable.js源码

  • web
  • webpack
  • make阶段
jonsam
2022-04-25
目录

make 阶段:module

在上一节中,我们追踪了 compilation.addEntry 函数,围绕着 compilation 探讨了 Module 对象的创建过程。在本节中,我们继续来探讨 module 的打包过程。

# 目录

  • 目录
  • _handleModuleBuildAndDependencies
  • buildModule
  • build
    • Build
    • Parse
  • JavascriptParser
  • 参考

# _handleModuleBuildAndDependencies

this.buildModule(module, (err) => {
 // ......
 // Module 已经在 processDependenciesQueue 中处理,直接 callback
 // This avoids deadlocks for circular dependencies
 if (this.processDependenciesQueue.isProcessing(module)) {
  return callback();
 }
 // 调用 processModuleDependencies 处理模块依赖
 this.processModuleDependencies(module, (err) => {
  // ......
  callback(null, module);
 });
});
1
2
3
4
5
6
7
8
9
10
11
12
13

这个函数调用 buildModule 打包模块,并且调用 processModuleDependencies 处理模块依赖。

# buildModule

buildModule(module, callback) {
  this.buildQueue.add(module, callback);
}
1
2
3

buildModule 将 module 封装成任务放入 buildQueue 队列,buildQueue 队列的 processor 为 _buildModule 。在 processor 中将处理 module。

下面我们赵忠看下 _buildModule 函数。

function _buildModule(module, callback) {
 // 检查 module 是否需要 build
 module.needBuild(
  {
   compilation: this,
   fileSystemInfo: this.fileSystemInfo,
   valueCacheVersions: this.valueCacheVersions,
  },
  (err, needBuild) => {
   // ......

   if (!needBuild) {
    // 不需要 build, 触发 CompilationHook.stillValidModule => call
    this.hooks.stillValidModule.call(module);
    return callback();
   }
   // 触发 CompilationHook.buildModule => call
   this.hooks.buildModule.call(module);
   // 将 module 加入到 builtModules
   // this.builtModules = new WeakSet();
   this.builtModules.add(module);
   // 开始 build
   module.build(
    this.options,
    this,
    this.resolverFactory.get("normal", module.resolveOptions),
    this.inputFileSystem,
    (err) => {
     // ......
    }
   );
  }
 );
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

因为每一个 module 对象都拥有自己的 module.needBuild 方法和 module.build 方法,下面以 NormalModule 为例继续探讨。

function needBuild(context, callback) {
 const { fileSystemInfo, compilation, valueCacheVersions } = context;
 // build if enforced
 if (this._forceBuild) return callback(null, true);

 // always try to build in case of an error
 if (this.error) return callback(null, true);
 // 配置 cacheable 和 snapshot 可以过滤掉部分无效的 build
 // always build when module is not cacheable
 if (!this.buildInfo.cacheable) return callback(null, true);

 // build when there is no snapshot to check
 if (!this.buildInfo.snapshot) return callback(null, true);

 // build when valueDependencies have changed
 /** @type {Map<string, string | Set<string>>} */
 const valueDependencies = this.buildInfo.valueDependencies;
 if (valueDependencies) {
  if (!valueCacheVersions) return callback(null, true);
  // 循环 buildInfo.valueDependencies,检查 context.valueCacheVersions 中依赖是否相同
  for (const [key, value] of valueDependencies) {
   if (value === undefined) return callback(null, true);
   const current = valueCacheVersions.get(key);
   if (
    value !== current &&
    (typeof value === "string" ||
     typeof current === "string" ||
     current === undefined ||
     !isSubset(value, current))
   ) {
    return callback(null, true);
   }
  }
 }

 // check snapshot for validity
 fileSystemInfo.checkSnapshotValid(this.buildInfo.snapshot, (err, valid) => {
  if (err) return callback(err);
  if (!valid) return callback(null, true);
  const hooks = NormalModule.getCompilationHooks(compilation);
  // 如果 buildInfo.snapshot 是合法的,触发 CompilationHook.needBuild => callAsync
  hooks.needBuild.callAsync(this, context, (err, needBuild) => {
   // ......
   callback(null, !!needBuild);
  });
 });
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

module.needBuild 是对 build 性能很好的优化手段。

# build

# Build

build 内部调用 _doBuild 。现在我们来看下 Build 的过程。

// lib/NormalModule.js
_doBuild(options, compilation, resolver, fs, hooks, callback) {
 // 创建 loaderContext,提供给 loader 的 context(方法和属性)
 const loaderContext = this._createLoaderContext(
  resolver,
  options,
  compilation,
  fs,
  hooks
 );
 // 初始化 fileDependencies、contextDependencies、missingDependencies 容器
 this.buildInfo.fileDependencies = new LazySet();
 this.buildInfo.contextDependencies = new LazySet();
 this.buildInfo.missingDependencies = new LazySet();
 this.buildInfo.cacheable = true;

 // 触发 CompilationHook.beforeLoaders => call
 hooks.beforeLoaders.call(this.loaders, this, loaderContext);
 // ......
 if (this.loaders.length > 0) {
  // 初始化 buildDependencies
  this.buildInfo.buildDependencies = new LazySet();
 }
 // 调用 loader-runner.runLoaders 加载源文件
 runLoaders(
  {
   // resource path
   resource: this.resource,
   // module loaders
   loaders: this.loaders,
   // module loader context
   context: loaderContext,
   // 处理资源的回调
   processResource: (loaderContext, resourcePath, callback) => {
    const resource = loaderContext.resource;
    // 获取 resource 的 scheme
    const scheme = getScheme(resource);
    // CompilationHook.readResource 是 HookMap,可使用 .for
    // 通过不同的 scheme 选择 HookMap 中的 Hook
    hooks.readResource
     .for(scheme)
     // 调用  CompilationHook.readResource.scheme => callAsync
     .callAsync(loaderContext, (err, result) => {
      // ......
      return callback(null, result);
     });
   },
  },
  (err, result) => {
   // ......
   // 收集 fileDependencies,contextDependencies 和 missingDependencies
   this.buildInfo.fileDependencies.addAll(result.fileDependencies);
   this.buildInfo.contextDependencies.addAll(result.contextDependencies);
   this.buildInfo.missingDependencies.addAll(result.missingDependencies);
   // 缓存 loader
   for (const loader of this.loaders) {
    this.buildInfo.buildDependencies.add(loader.loader);
   }
   this.buildInfo.cacheable = this.buildInfo.cacheable && result.cacheable;
   // 处理 loader 返回的结果
   processResult(err, result.result);
  }
 );
}

const processResult = (err, result) => {
 // ......
 // 获取 source、sourceMap 和 extraInfo
 const source = result[0];
 const sourceMap = result.length >= 1 ? result[1] : null;
 const extraInfo = result.length >= 2 ? result[2] : null;

 // ......
 // 创建 RawSource 对象
 this._source = this.createSource(
  options.context,
  this.binary ? asBuffer(source) : asString(source),
  sourceMap,
  compilation.compiler.root
 );
 // ......
 // 如果 extraInfo 中提供了 AST,则缓存
 this._ast =
  typeof extraInfo === "object" &&
  extraInfo !== null &&
  extraInfo.webpackAST !== undefined
   ? extraInfo.webpackAST
   : null;
 return callback();
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90

核心作用如下:

  • runLoaders 调用 loader-runner 包的方法处理源文件,将源文件处理为 webpack 识别的 JavaScript 源代码。创建 loaderContext 可传递给 loader 使用。
  • loader 处理的结果包含 Dependencies 和 content。Dependencies 将收集到 fileDependencies,contextDependencies 和 missingDependencies。content 将处理为 RawSource 对象。content 可能为 string 或者 buffer(二进制)。

# Parse

build(options, compilation, resolver, fs, callback) {
 // ......
 // 调用 _doBuild 完成 Build 工作,在回调中完成 Parse 工作。
 return this._doBuild(options, compilation, resolver, fs, hooks, (err) => {
  // 触发 CompilationHook.beforeParse => call
  hooks.beforeParse.call(this);
  // 如果配置了 module.noParse,退出 parse
  const noParseRule = options.module && options.module.noParse;
  if (this.shouldPreventParsing(noParseRule, this.request)) {
   // We assume that we need module and exports
   this.buildInfo.parsed = false;
   this._initBuildHash(compilation);
   return handleBuildDone();
  }

  let result;
  // 获取到源代码(string | buffer)
  const source = this._source.source();
  // 使用 Parser parse 源代码为 AST
  // 注意:result 不是 AST 而是 state,AST 在 JavaScriptParser parse 中 触发 JavaScriptParserHook.finish
  result = this.parser.parse(this._ast || source, {
   source,
   current: this,
   module: this,
   compilation: compilation,
   options: options,
  });
  handleParseResult(result);
 });
}

const handleParseResult = (result) => {
 // 依赖排序
 this.dependencies.sort(
  concatComparators(
   compareSelect((a) => a.loc, compareLocations),
   keepOriginalOrder(this.dependencies)
  )
 );
 // 初始化 build hash
 this._initBuildHash(compilation);
 this._lastSuccessfulBuildMeta = this.buildMeta;
 // 完成 build
 return handleBuildDone();
};

const handleBuildDone = () => {
 // 触发 CompilationHook.beforeSnapshot => call
 hooks.beforeSnapshot.call(this);

 // 获取创建快照的 options
 const snapshotOptions = compilation.options.snapshot.module;
 // 如果没有配置 cacheable 或者 snapshotOptions 直接 callback
 if (!this.buildInfo.cacheable || !snapshotOptions) {
  return callback();
 }
 // ......
 // 检查依赖
 checkDependencies(this.buildInfo.fileDependencies);
 checkDependencies(this.buildInfo.missingDependencies);
 checkDependencies(this.buildInfo.contextDependencies);
 //  ......
 // 为依赖(file/context/missingDependencies)创建快照,写入 filesystem
 // convert file/context/missingDependencies into filesystem snapshot
 compilation.fileSystemInfo.createSnapshot(
  startTime,
  this.buildInfo.fileDependencies,
  this.buildInfo.contextDependencies,
  this.buildInfo.missingDependencies,
  snapshotOptions,
  (err, snapshot) => {
   // ......
   // 清空依赖并缓存快照
   this.buildInfo.fileDependencies = undefined;
   this.buildInfo.contextDependencies = undefined;
   this.buildInfo.missingDependencies = undefined;
   this.buildInfo.snapshot = snapshot;
   return callback();
  }
 );
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81

总结 build 的核心作用:

  • 调用 _doBuild 完成 Build 工作,在回调中完成 Parse 工作。Build 就是使用匹配的 Loader 加载源文件为 source,Parse 就是根据源码生成 AST。
  • 如果配置了 module.noParse,会跳过 Parse 过程。
  • Parser.parse 将 source 转换为 AST。
  • NormalModule 是在 NormalModuleFactory 中创建的,在 compiler.compile 中 newCompilationParams 函数中会创建 normalModuleFactory。NormalModule 在初始化时在 NormalModuleFactoryHook.factorize 触发 NormalModuleFactoryHook.resolve,在回调里执行 getParser。在调用 NormalModuleFactory.create 时即 _factorizeModule 中 factory.create 时触发此钩子。
  • parser 和 generator 是在内置插件 JavascriptModulesPlugin 中加载的,在 CompilerHook.compilation 中 normalModuleFactoryHook.createParser 和 normalModuleFactoryHook.createGenerator 分别创建 JavascriptParser 和 JavascriptGenerator 。参见 lib/javascript/JavascriptModulesPlugin.js 和 lib/NormalModuleFactory.js 。

# JavascriptParser

// lib/javascript/JavascriptParser.js
const { Parser: AcornParser } = require("acorn");
const { importAssertions } = require("acorn-import-assertions");
const parser = AcornParser.extend(importAssertions);

parse(source, state) {
  // ......
  const ast = JavascriptParser._parse(source, {
    sourceType: this.sourceType,
    onComment: comments,
    onInsertedSemicolon: pos => semicolons.add(pos)
  });
  // ......
  // JavaScriptParserHook.program 为 SyncBailHook 
  if (this.hooks.program.call(ast, comments) === undefined) {
   // 标记内部标记 isStrict、isAsmJs
   this.detectMode(ast.body);
   // preWalk AST
   this.preWalkStatements(ast.body);
   // 重置 prevStatement
   this.prevStatement = undefined;
   // blockPreWalk
   this.blockPreWalkStatements(ast.body);
   // 重置 prevStatement
   this.prevStatement = undefined;
   // Walk AST
   this.walkStatements(ast.body);
  }

  // 触发 JavaScriptParserHook.finish => call
  this.hooks.finish.call(ast, comments);
  return state;
}

_parse(code, options) {
 // ......
 const ast = parser.parse(code, parserOptions);
 // ......
 return ast;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

通过如上的过程,我们了解到 Module 将会经过 Build 和 Parse 的过程,这两个过程分别将文件加载为源代码、将源代码转换为 AST。Build 的过程是经由 loader-runner 包完成的,而 Parse 包是经由 acorn 包完成的。注意:由代码字符串生成 AST 的具体过程,此处不详细探讨,在 acorn 的源码解析中,我们会详细探讨。

# 参考

  • xtuc/acorn-import-assertions: Support for import assertions in acorn (opens new window)
编辑 (opens new window)
上次更新: 2022/09/06, 14:25:16
make 阶段:compilation
make 阶段:walk

← make 阶段:compilation make 阶段:walk→

最近更新
01
渲染原理之组件结构与 JSX 编译
09-07
02
计划跟踪
09-06
03
开始上手
09-06
更多文章>
Theme by Vdoing | Copyright © 2022-2022 Fancy Front End | Made by Jonsam by ❤
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式