webpack 4 源碼主流程分析(七):構建 module(下)

原文首發於 blog.flqin.com。若有錯誤,請聯繫筆者。分析碼字不易,轉載請代表出處,謝謝!html

parse 源碼

runLoaders 運行結束後,在回調裏執行了 createSource 後,判斷 loaderresult 是否有第三個參數對象而且裏面存在 webpackAST 屬性,若是有則爲 ast 賦值到 _ast 上。node

而後回到 this.doBuild 執行回調,在根據項目配置項判斷是否須要 parse 後,若須要解析,則執行:webpack

const result = this.parser.parse(
  this._ast || this._source.source(),
  {
    current: this,
    module: this,
    compilation: compilation,
    options: options
  },
  (err, result) => {
    //...
  }
);
if (result !== undefined) {
  // parse is sync
  handleParseResult(result);
}
複製代碼

this.parser 便是在 reslove 流程 裏的組合對象裏獲得的 parsergit

this.parser.parse,在該方法裏若是 this._ast 不存在則傳 this._source._value 即代碼字符串。 而後進入文件 node_modules/webpack/lib/Parser.js 執行 Parser.parsegithub

parser.parse

析出 ast

方法裏執行:web

ast = Parser.parse(source, {
  sourceType: this.sourceType,
  onComment: comments
});
複製代碼

Parser.parse 即爲 Parser 靜態方法,該方法裏主要執行:json

ast = acornParser.parse(code, parserOptions); //即 acorn.Parser
複製代碼

webpack 經過 acorn 獲得源碼對應的 astast 相關資料:api

遍歷 ast 收集依賴

回到 Parser.parse ,對 ast 進行遍歷,執行:數組

if (this.hooks.program.call(ast, comments) === undefined) {
  this.detectStrictMode(ast.body);
  this.prewalkStatements(ast.body);
  this.blockPrewalkStatements(ast.body);
  this.walkStatements(ast.body);
}
複製代碼
  • this.hooks.program.call(ast, comments)app

    觸發回調 plugin(HarmonyDetectionParserPluginUseStrictPlugin) 根據是否有 import/exportuse strict 增長依賴:HarmonyCompatibilityDependency, HarmonyInitDependencyConstDependency

  • this.detectStrictMode(ast.body)

    檢測當前執行塊是否有 use strict,並設置 this.scope.isStrict = true

  • this.prewalkStatements(ast.body)

    • 處理 import 進來的變量,是 import 就增長依賴 HarmonyImportSideEffectDependencyHarmonyImportSpecifierDependency;
    • 處理 export 出去的變量,是 export 增長依賴 HarmonyExportHeaderDependencyHarmonyExportSpecifierDependency
    • 還會處理其餘相關導入導出的變量
  • this.blockPrewalkStatements(ast.body)

    處理塊遍歷

  • this.walkStatements(ast.body)

    用於深刻函數內部(方法在 walkFunctionDeclaration 進行遞歸),而後遞歸繼續查找 ast 上的依賴,異步此處深刻會增長依賴 ImportDependenciesBlock;

上述執行結束後,會根據 import/export 的不一樣狀況即模塊間的相互依賴關係,在對應的 module.dependencies 上增長相應的依賴。

各依賴做用解釋

在後面 generaterender 階段,調用這些依賴(Dependency)對應的 template.apply 來渲染生成代碼資源。

demo 入口文件 a.jsc.js 爲例,則依賴爲:

//a.js module
{
  "dependencies": [
    "HarmonyCompatibilityDependency", //對應模板 `HarmonyExportDependencyTemplate` 會在源碼裏最前面添加如:`__webpack_require__.r(__webpack_exports__);` 的代碼,用於定義 exports:__esModule
    "HarmonyInitDependency", // 對應模板 `HarmonyInitDependencyTemplate`, 下文單獨說明其做用
    "ConstDependency", // 對應模板 `ConstDependencyTemplate` 操做會在源碼裏將同步 import 語句刪掉
    "HarmonyImportSideEffectDependency", //對應模板 `HarmonyImportSideEffectDependencyTemplate`,執行 apply 調用父類 HarmonyImportDependencyTemplate 的 apply,即爲空。
    "HarmonyImportSpecifierDependency" //對應模板 `HarmonyImportSpecifierDependencyTemplate`,會在源碼裏將引入的變量替換爲 webpack 對應的包裝變量
  ],
  "blocks": ["ImportDependenciesBlock"] //異步模塊  對應模板 `ImportDependencyTemplate`, 會在源碼裏將本 demo 中的 `import('./c.js')`替換爲 `Promise.resolve(/*! import() */).then(__webpack_require__.bind(null, /*! ./c.js */ "./src/c.js"))`
}
複製代碼
//d.js module
{
  "dependencies": [
    "HarmonyCompatibilityDependency",
    "HarmonyInitDependency",
    "HarmonyExportHeaderDependency", // 對應模板 `HarmonyExportDependencyTemplate` 會在源碼裏將關鍵字 export 刪掉
    "HarmonyExportSpecifierDependency" //對應模板 `HarmonyExportSpecifierDependencyTemplate` 執行 apply 爲空
  ],
  "blocks": []
}
複製代碼

HarmonyInitDependency

執行其對應 template.apply(文件 HarmonyInitDependency.js)中,先遍歷 module.dependencies,判斷各依賴對應的 template 是否包含 harmonyInitgetHarmonyInitOrder 函數(用於導入的 import 排序),若都存在,則執行:

const order = template.getHarmonyInitOrder(dependency);
複製代碼

執行對應的 template.getHarmonyInitOrder 用於獲取排序的 order,在不一樣的依賴里根據須要可能會返回 NaN(如 HarmonyImportSideEffectDependencyTemplate 裏判斷無反作用(sideEffects)就會返回 NaN),最終篩選出不是 NaN 的依賴組成數組 list,即爲含有 importexport 的依賴,按 order 排序後, 執行:

for (const item of list) {
  item.template.harmonyInit(item.dependency, source, runtime, dependencyTemplates);
}
複製代碼

執行對應的 template.harmonyInit ,對應模板在源碼前加入如下代碼:

  • export 相關(HarmonyExportSpecifierDependency
/* harmony export (binding) */

__webpack_require__.d(__webpack_exports__, 'mul', function() {
  return mul;
});
複製代碼
  • import 相關(HarmonyImportSideEffectDependencyHarmonyImportSpecifierDependency
/* harmony import */

var Src_b__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! Src/b */ './src/b.js');
複製代碼

生成 buildHash

parse 結束後,在 handleParseResult 裏執行 this._initBuildHash(compilation)

_initBuildHash(compilation) {
    const hash = createHash(compilation.outputOptions.hashFunction); // compilation.outputOptions.hashFunction : md4
    if (this._source) {
        hash.update("source");
        this._source.updateHash(hash); // this._value
    }
    hash.update("meta");
    hash.update(JSON.stringify(this.buildMeta));
    this._buildHash = /** @type {string} */ (hash.digest("hex"));
}
複製代碼

webpack 採用 nodejs 提供的加密模塊 crypto 進行 hash 加密。

  • createHash(): 即執行 new BulkUpdateDecorator(require("crypto").createHash(algorithm))
  • hash.update(): 更新 hash 內容
  • hash.digest("hex"):獲得 hash

先初始化了 hash, 而後分別 updatesourcethis._valuethis._source.updateHash(hash) 得到,爲文件源碼),metathis.buildMeta,最後計算出結果賦給 this._buildHash

而後回到文件 Compilation.jsmodule.build 的回調。對 errorwarning 的處理後,對 module.dependencies 按照代碼在文件中出現的前後順序進行排序,而後觸發 Compilation.hooks: succeedModule

遞歸解析依賴

而後執行回調回到 this.buildModule 的回調裏執行 afterBuild

const afterBuild = () => {
  if (addModuleResult.dependencies) {
    this.processModuleDependencies(module, err => {
      if (err) return callback(err);
      callback(null, module);
    });
  } else {
    return callback(null, module);
  }
};
複製代碼

即判斷若是該模塊是首次解析則執行 processModuleDependencies

一旦某個模塊被解析建立後,在 this.addModule(module)(上文已提到)裏會設置 addModuleResult.dependenciesfalse 便可以免該模塊重複解析建立依賴。

processModuleDependencies 裏,對 muduledependencies, blocks(懶加載 import xx 會存入), variables(內部變量 __resourceQuery )分別處理,其中對 blocks 的處理會遞歸調用。整理過濾沒有標識 Identifiermodule,獲得 sortedDependencies(以 module a 爲例):

sortedDependencies = [
  {
    factory: NormalModuleFactory,
    dependencies: [HarmonyImportSideEffectDependency, HarmonyImportSpecifierDependency]
  },
  {
    factory: NormalModuleFactory,
    dependencies: [ImportDependency]
  }
];
複製代碼

而後調用 this.addModuleDependencies:

addModuleDependencies(module, dependencies, bail, cacheGroup, recursive, callback) {
// dependencies 即爲上文中的 sortedDependencies
//...
  asyncLib.forEach(
      dependencies,
      (item, callback) => {
        //...
        const semaphore = this.semaphore;
        semaphore.acquire(() => {
          const factory = item.factory;
          factory.create(
            {
                //...
            },(err, dependentModule) => {
                // 回調內容
            }
          );
        });
      },
      err => {
        //...
        return process.nextTick(callback);
      }
  );
}
複製代碼

經過 asyncLib.forEach forEach 會將回調傳給 iterator,在出現 erriterator 所有執行後執行回調。

批量調用每一個依賴的 NormalModuleFactory.create,即與前文moduleFactory.create 功能一致。因此重複開始走 reslove 流程

NormalModuleFactory.create -> resolve流程 -> 初始化module -> add module -> module build -> afterBuild -> processModuleDependencies
複製代碼

就這樣,從入口 module 開始,根據 module 間的依賴關係,遞歸調用將全部的 module 都轉換編譯。

入口 module 生成

在依賴轉換完成後,執行:

return process.nextTick(callback);
複製代碼

將在 nodejs 下一次事件循環時調用 callback 即執行 this.processModuleDependencies 的回調:

this.processModuleDependencies(module, err => {
  if (err) return callback(err);
  callback(null, module);
});
複製代碼

此時返回一個入口 module

{
  "module": {
    //...
    //同步模塊
    "dependencies": ["HarmonyImportSideEffectDependency", "HarmonyImportSpecifierDependency"],
    //異步模塊
    "blocks": ["ImportDependenciesBlock"]
  }
}
複製代碼

執行 this._addModuleChain 的回調,觸發 compilation.hooks:succeedEntry, 到此 mudule 生成結束!

本章小結

  1. 調用 parser 將前面 runloaders 的編譯結果經過 acorn 轉換爲 ast
  2. 遍歷 ast 根據導入導出及異步的狀況觸發相關鉤子插件收集依賴,這些依賴用於解析遞歸依賴和模板操做;
  3. 根據每一個 module 的相關信息生成各自惟一的 buildHash
  4. 根據 module 間的相互依賴關係,遞歸解析全部依賴 module,最終返回一個入口 module
相關文章
相關標籤/搜索