多程序打包:thread-loader 原始碼(13)

語言: CN / TW / HK

theme: fancy highlight: an-old-hope


持續創作,加速成長!這是我參與「掘金日新計劃 · 6 月更文挑戰」的18天,點選檢視活動詳情

碼字不易,感謝閱讀,你的點贊讓人振奮!
如文章有誤請移步評論區告知,萬分感謝!
未經授權不得轉載!

一、前情回顧

上文詳細討論了 onMessage 的程式碼結構和程式碼的意義,另外討論了其中的細節問題如下:

  1. onMessage 分為三種類型的訊息:
    • 1.1 代表任務的 job,任務 pushqueue
    • 1.2 代表結果的 result,接收父程序的返回結果
    • 1.3 代表預熱的 warmup,把需要的模組載入到子程序節約時間;
  2. 建立 queue 時傳入的 worker 函式負責跑 loaderworker 函式細節並未展開討論;

  3. typeresult 是程序間通訊的一環,子程序中的 loaders 需要了某些方法,礙於程序通訊無法傳遞方法,所以委託父程序去呼叫,再通過回撥把結果給到子程序;

本篇小作文正式講解跑 loaderworker 函式的具體實現細節!

二、asyncQueue 的 worker 函式

asyncQueue 來自 neo-async/queue.js,老相識了,後面要說的是它的第一個引數 worker 函式,也是 thread-loader 中最終執行 loader 的函數了。

2.1 回顧 neo-async/queue.js

在討論父程序程式碼中 WorkerPool 的時候詳細講述過 neo-async/queue 的工作原理,他接收一個 worker 函式和一個表示併發數目的數字,返回一個佇列 queue

當有資料被 pushqueue 時,neo-async 會呼叫 runQueue 方法消耗佇列,runQueue 內部會呼叫建立 queue 時傳入 worker 函式處理 data,並且給 worker 函式傳入一個 done 方法,這個 done 將會在 worker 函式執行到結束時呼叫,意在告知 queue 本次 worker 函式執行已經結束。

在呼叫 runQueue 的過程中需要判斷當前已經在執行的任務是否超出建立 queue 時傳入的併發數限制,如果超過了就會暫停。

js const queue = asyncQueue(({ id, data }, taskCallback) => { // 這個箭頭函式就是 worker 函數了 // taskCallback 就是 runQueue 的 done 方法 }, PARALLEL_JOBS);

關於 neo-async/queue 暫時就說這麼多,本文的重點是 worker 函式;

2.2 worker 函式程式碼結構

2.2.1 引數:

  1. { id, data },這個位置的引數就是前面 pushqueue 裡面的 data
  2. taskCallbackrunQueue 裡面傳入的 done

2.2.2 方法內部邏輯:

  1. 建立 resolveWithOptions 方法,這個方法是下面 runLoadersloaderContext.resolve 方法的實現;
  2. 宣告常量 buildDependencies 陣列
  3. 呼叫 loaderRunner.runLoaders 方法,傳入 runLoaders 所需引數(包含 loaderContext 物件)、接收 loader 結果的回撥函式,這些引數後面會詳細討論;

```js const queue = asyncQueue(({ id, data }, taskCallback) => { // taskCallback 就是 runQueue 的 done try { // loaderContext.resolve 方法的實現 const resolveWithOptions = (context, request, callback, options) => {};

// 儲存本次 runLoaders 得到的 buildDependencies
const buildDependencies = [];

// 呼叫 loaderRunner.runLoaders 跑 loaders
loaderRunner.runLoaders(
  {
    // runLoaders 所需的選項物件,包含一個模擬出來的 loaderContext
    loaders: data.loaders,
    context: { /* 模擬出來的 loaderContext */ }
  },
  (err, lrResult) => {
     // 處理 loader 執行結束後的結果
     // 這個函式給他取個名字,後面叫他 loader 結果回撥
  }
 );

} catch (e) {

taskCallback();

} }, PARALLEL_JOBS); ```

三、 runLoader 方法

  1. 方法位置:node_modules/loader-runner/lib/LoaderRunner.js

  2. 方法引數:

    • 2.1 options: 選項物件,這裡麵包含要執行的 loaderloaderContext 物件,這個 options 物件是我們下午要討論的一個 重點;
    • 2.2 callback: 當 loader 執行結束後要執行的回撥函式,也就是我們上面說的 loader 結果回撥
  3. 方法作用:這裡並不會具體討論 runLoaders 的全部工作,為了便於讓大家理解這個選項物件的作用才臨時加入了這個片段。推薦另一篇詳細介紹 webpack 執行 loader 過程的文章:包看包會: webpack run loader

    • 3.1 從 options 上獲取傳入的 context 屬性,即 loaderContext,對其進行擴充套件,包括 addDependenciesasync、等方法都是在這個時間點擴充套件的;
    • 3.2 呼叫 iteratePitchingLoaders 方法並傳入經過擴充套件的 loaderContext 物件。這個 iteratePitchingLoaders 方法加重 loader 模組,並執行 loaderpitch 方法,這個階段也就是 pitch 階段。當 pitch 階段執行結束後自動進入 normal 階段,在 normal 結束後呼叫 callback 並把 loader 執行的結果傳遞給 callback

```js exports.runLoaders = function runLoaders(options, callback) {

var loaderContext = options.context || {};

// 擴充套件 loaderContext
loaderContext.dependency = loaderContext.addDependencies = function addDependency () {
}

// ....

// 呼叫 iteratePitchingLoaders 載入並執行 loader 的 pitch 方法,
// 進入 pitch 階段;
// pitch 階段結束後自動進入 normal 階段,
// 結束後呼叫 callback 回撥即 loader 結果回撥
iteratePitchingLoaders(processOptions, loaderContext, function (err, result) {
    // 呼叫 runLoaders 回撥傳入 result
    callback(null, {
     result: result,
     resourceBuffer: processOptions.resourceBuffer,
     cacheable: requestCacheable,
     fileDependencies: fileDependencies,
     contextDependencies: contextDependencies
    });
}

} ```

3.1 webpack 呼叫 runLoaders

之所以提這個點,也是便於大家理解 worker 做這些工作的初衷,下圖是 webpack 內部呼叫的 runLoaders 方法,圖中的 context: loaderContext 就是 webpack 內部初始化的 loaderContext 這個上下文物件,也就是 loader 函式內部 this 所繫結的物件,上文件傳送門

image.png

3.2 worker.js 中的 loaderContext vs webpack 的 loaderContext

大家思考一個問題,為什麼在這裡呼叫 runLoader 傳入的 context 是一個新構造的物件,而不是 webpack 中的 loaderContext 物件(在作用上這兩個物件時等價的)?

如果你很快就反應出答案,說明前面的內容你已經滾瓜爛熟了:是因為在 thread-loader 中呼叫 runLoaders 這個方法是在 worker.js 中呼叫的,而 worker.js 又是在子程序中的呼叫。礙於程序間通訊的限制,程序間通訊使用的自定義管道的方式實現的,而這種實現方式傳遞的是被序列化的 JSON 字串。

這就導致 webpack 中的 loaderContext 物件無法被傳遞到子程序中,究其根本,是因為程序間的記憶體是隔離的,webpackloaderContext 物件存在於父程序,而 runLaoders 卻是在子程序中。所以當子程序需要時,只能再造一個新的物件。

這個新造的物件包含了 loaderContext 應有的屬性和方法,但是這些方法並不直接處理工作,而是轉發這些工作到父程序讓父程序完成,父程序完成後把結果傳送給子程序。

這裡就揭示了這個全新的 loaderContext 的核心實現,雖然這裡沒有程式碼,但是請記住,這個核心:轉發工作個父程序,等待接收父程序傳送來的結果。

3.3 webpack loaderContext

這裡就偷個懶上個截圖吧,只需要關注一些方法和屬性,後面的 worker.js 會同樣實現一份這樣的方法和屬性出來;

image.png

3.4 worker.js loaderContext

```js let cfg = { loaders: data.loaders, // 要跑的 loader resource: data.resource, // readResource: fs.readFile.bind(fs), context: { // worker.js 的 loaderContext 物件 version: 2, fs,

  // 模擬 loaderContext 的 loadModule 方法
  loadModule: (request, callback) => {},

  // 模擬 loaderContext 的 resolve 方法
  resolve: (context, request, callback) => {},

 // 模擬 loaderContext 的 getResolve 方法
  getResolve: (options) => (context, request, callback) => {},

  // 模擬 loaderContext 的 getOptions 方法
  getOptions(schema) {},

  // 模擬 loaderContext 的 emitWarning 方法
  emitWarning: (warning) => {},

  // 模擬 loaderContext 的 emitError 方法
  emitError: (error) => {},

  // 模擬 loaderContext 的 exec 方法
  exec: (code, filename) => {},

  // 模擬 loaderContext addBuildDependency 方法
  addBuildDependency: (filename) => {},
  options: {},
  webpack: true,
  'thread-loader': true,
  sourceMap: data.sourceMap,
  target: data.target,
  minimize: data.minimize,
  resourceQuery: data.resourceQuery,
  rootContext: data.rootContext
},

} ```

四、總結

本篇小作文討論了一下 worker.js 中以下功能: 1. 用於控制併發建立的 queue 的 worker 函式的程式碼結構和大致功能; 2. 另外還討論了 runLoaders 方法,接收 optionscallbackloader結果函式); 3. 期間還討論了 loaderContext 作用,還對比了 worker.jsloaderContext 物件和 webpackloaderContext 物件; 4. 藉助兩個 loaderContext 回顧了程序間通訊,還鋪墊了 worker.js 中實現的 loaderContext 上的方法核心;