持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的7天,点击查看活动详情
一、前情回顾
从上一篇小作文开启了一个新坑,上文主要讨论了以下几个问题:
thread-loader
作用;thread-loader
的入口文件;- 复习
loader.pitch & normal
以及两者的顺序; thread-loader
利用pitch
截取后面的loader
扔到线程池;
本篇我们开始讨论 thread-loader
的内部实现细节,期间会涉及到相当多的准备知识,没有这些准备知识又会导致看不懂,不得已在讲代码的过程中会多次穿插,请看官老爷见谅,后期有可能会重新组织行文顺序。
二、src/index.js
// loader-utils 是个 webpack 用的工具库,包含很多的 loader 工具
import loaderUtils from 'loader-utils';
import { getPool } from './workerPools';
function pitch() {
}
function warmup(options, requires) {
}
export { pitch, warmup }; // eslint-disable-line import/prefer-default-export
三、src/index.js function pitch
function pitch() {
// 获取 loader 的配置选项
const options = loaderUtils.getOptions(this);
// 传入选项获取 workerPool
const workerPool = getPool(options);
if (!workerPool.isAbleToRun()) {
return;
}
// 调用 this.async 将当前 loader 变成一个 异步 loader
const callback = this.async();
// 调用 workerPool.run 执行 thead-loader 后面的 loader
// this.loaderIndex + 1 就是不包含 thread-loader
workerPool.run(
{ // 这个对象称为 data
// loaders 是除了 thread-loader 以外的 loader
loaders: this.loaders.slice(this.loaderIndex + 1).map((l) => {
return {
loader: l.path, options: l.options, ident: l.ident,
};
}),
// ...
},
(err, r) => {
// 这个回调称为 cb
// ...
}
);
}
前面说过 pitch
的作用,是作为 thread-loader
在 pitching
阶段调用的方法,用于截取 thread-loader
后面的 loader
并把它们放到 worker pool
中,接着看看这个方法的具体实现;
- 使用
loaderUtils
获取在webpack.config.js
中设置该loader
时传入的参数,注意loader
中的this
是loaderContext
对象,例如下面例子中的{ something: 'something' }
这个对象;
module.exports = {
entry: './src/index.js',
mode: 'development',
// ...
module: {
rules: [
{
test: /.js$/,
exclude: /node_modules/,
use: [
{
loader: 'thread-loader',
// 这个对象就是 loaderUtils.getOptions(this) 得到的对象
options: {
something: 'something'
}
}
// ....
]
}
]
},
// ...
}
-
调用
this.async
方法将该loader
变成一个异步的loader
;this.async
返回的callback
会在异步处理完成后调用,以继续调用后面的loader
(this.loaderIndex++
) -
调用
workerPool.run
方法,传入一个对象和会回调(对象我们称之为data
,回调暂且叫做cb
);workerPool.run
结束后调用的方法;- 3.1 workerPoo.run 的 data 对象,这个对象有点像 loader 被调用时接收到 loaderContext 对象
{ loaders: ..., // 除 thread-loader 以外的 loader resource: this.resourcePath + (this.resourceQuery || ''), // 当前模块的资源路径 sourceMap: this.sourceMap, emitError: this.emitError, emitWarning: this.emitWarning, loadModule: this.loadModule, resolve: this.resolve, getResolve: this.getResolve, target: this.target, minimize: this.minimize, resourceQuery: this.resourceQuery, // request 后面的 query optionsContext: this.rootContext || this.options.context, // 项目的根目录 rootContext: this.rootContext, // 同样是项目根目录,也是 webpack 的 context 目录 },
- 3.2 workerPool.run 的 cb 回调函数
(err, r) => { // 这个 r 是 run 方法的返回值, // r.result 是模块的代码,r.xxxDependencies 是该依赖的各类型的子依赖 // cb 方法的作用就是把得到的 r 中的各种类型的子依赖添加到该模块对应类型的依赖下 // 例如在我们的例子中就是 fileDependencies 文件依赖 if (r) { r.fileDependencies.forEach(d => this.addDependency(d)); r.contextDependencies.forEach(d => this.addContextDependency(d)); r.missingDependencies.forEach(d => this.addMissingDependency(d)); r.buildDependencies.forEach(d => // Compatibility with webpack v4 this.addBuildDependency ? this.addBuildDependency(d) : this.addDependency(d)); } if (err) { callback(err); return; } // callback 是 this.async 的返回值,用于告知 webpack 这个 loader 跑完了, // 进行后续的 loader 调用或者后续的打包工作 callback(null, ...r.result); }
workerPool.run()
的cb
接收到的结果r
四、 getPool 和 workerPool 实例
workerPool
是在 thread-loader
的 pitch
方法中调用 getPool
方法得到的 WorkerPool
类的实例;先解释个名词 workerPool
,字面意思,”worker池(工作进程池)“
,也就是说这个 workerPool
是保存和调度“worker”
的。后面还有一个 poolWorker
,这个名词就是池子中的 worker
,是干活儿的 worker
;
4.1 pitch 方法调用 getPool
- 方法位置:src/index.js
function pitch() {
// getPool 创建或者获取当前 options 对应的 workerPool
const workerPool = getPool(options);
}
4.2 getPool 方法
- 方法位置:
src/workerPools.js
- 方法参数:
thread-loader
在webpack.config.js
中的配置信息 - 方法作用:
-
- 根据接收到的
options
选项格式化workerPoolOptions
选项对象,这个对象中包含新建 worker 进程和调度worker池子的数据,如numberOfWorkers
,workerNodeArgs
,poolTimeout
...
- 根据接收到的
-
- 将上述得到的
workerPoolOptions
转成字符串,这个字符串将作为缓存子进程的key
- 将上述得到的
-
- 尝试从
workerPools
缓存对象中读取缓存,如果没有缓存就新建WorkerPool
的实例并缓存
- 尝试从
-
function getPool(options) {
const workerPoolOptions = {
name: options.name || '',
numberOfWorkers: options.workers || calculateNumberOfWorkers(),
workerNodeArgs: options.workerNodeArgs,
workerParallelJobs: options.workerParallelJobs || 20,
poolTimeout: options.poolTimeout || 500,
poolParallelJobs: options.poolParallelJobs || 200,
poolRespawn: options.poolRespawn || false,
};
// 选项对象作为 key
const tpKey = JSON.stringify(workerPoolOptions);
// 根据 workerPoolOptions 缓存已经创建的 worker
workerPools[tpKey] = workerPools[tpKey] || new WorkerPool(workerPoolOptions);
// 返回 WorkerPool 实例
return workerPools[tpKey];
}
五、 WokerPool 类
export default class WorkerPool {
constructor(options) {
this.options = options || {};
this.numberOfWorkers = options.numberOfWorkers; // 最大工作进程数
this.poolTimeout = options.poolTimeout; // 进程闲置时长,超过后会 kill worker 进程
this.workerNodeArgs = options.workerNodeArgs; // 传递给子进程的 process argv 参数
this.workerParallelJobs = options.workerParallelJobs;
this.workers = new Set();
this.activeJobs = 0;
this.timeout = null;
// 调度 worker 的异步队列
this.poolQueue = asyncQueue(
this.distributeJob.bind (this), // 创建 worker 并分配任务给 worker
options.poolParallelJobs // 并发数
);
this.terminated = false;
this.setupLifeCycle();
}
run(data, callback) {
// ...
}
distributeJob(data, callback) {
// ...
}
createWorker() {
// ...
}
}
5.1 asyncQueue
asyncQueue
来自另一个知名库 neo-async。这个东西用于创建一个异步队列,并且提供了向队列中插入、移除、遍历的方法,方便进行异步队列的管理。
import asyncQueue from 'neo-async/queue';
5.1.1 neo-async/queue.js
'use strict';
module.exports = require('./async').queue;
可以看出 neo-async/queue.js
中导出的 queue
是 async.js
导出对象上的一个属性
5.1.2 async.js
async.js
中导出的内容量十分巨大,这里我们只关注 queue
(function(global, factory) {
/*jshint -W030 */
'use strict';
typeof exports === 'object' && typeof module !== 'undefined'
? factory(exports)
: typeof define === 'function' && define.amd
? define(['exports'], factory)
: global.async
? factory((global.neo_async = global.neo_async || {}))
: factory((global.async = global.async || {}));
})(this, function(exports) {
// 这个 index 是最终导出对象
var index = {
queue: queue
};
// 导出 index
export['default'] = index;
// queue 的实现函数
function queue(worker, concurrency) { }
})
5.1.3 async.js -> function queque
function queue(worker, concurrency) {
return baseQueue(true, worker, concurrency);
}
async.js
导出的 queue
是 baseQueue
的一个科里化函数,所以真正的实现逻辑还要往下看 baseQueue
方法
5.1.4 async.js -> function baseQueue
function baseQueue(isQueue, worker, concurrency, payload) {
if (concurrency === undefined) {
concurrency = 1;
} else if (isNaN(concurrency) || concurrency < 1) {
throw new Error('Concurrency must not be zero');
}
var workers = 0;
var workersList = [];
var _callback, _unshift;
var q = {
// .... q 有很多属性,暂时忽略掉,更容易理解主线
// DLL 是动态双向链表
_tasks: new DLL(),
// 向链表中插入项
push: push,
// 队列的处理方法,isQueue 为 true,是上面 function queue 传入的 baseQueue(true, ....)
process: isQueue ? runQueue : runCargo,
idle: idle, // 获取是否空闲,不重要
// 这个 worker 就是前面 WokerPool 实例创建时传入的 this.distributeJob.bind(this)
_worker: worker
};
return q;
function push(tasks, callback) {
_insert(tasks, callback);
}
function _exec(task) { }
function _insert(tasks, callback, unshift) {}
function _next(q, tasks) { }
function runQueue() { }
}
5.1.5 总结一下 asyncQueue
虽然还没有介绍 queue
的具体运行原理,但是这里需要先剧透介绍一下,咱们的小作文是按照代码执行顺序组织的,介绍一下 queue
的原理,后面理解起来就会轻松很多。
首先是 thread-loader
的 pitch
方法创建 WorkerPool
实例,而创建 WokerPool
实例执行构造函数时会在 WorkerPool
实例上初始化 queue
属性,this.queue = asyncQueue(worker, concurency)
。
asyncQueue
接收到 worker
和 concurency
后会初始化 q
并返回对象,q
包含以下几个重点属性和方法:
_tasks
:用双向链表实现的队列,其中用于存放待遍历的data
数据;push
:向双向链表添加data
的的方法,push
后会启动消耗_tasks
中的队列process
:消耗队列的方法,在当前场景下对应的runQueue
方法,这个方法中会调用上面asyncQueue
收到的worker
函数,并传给_tasks
中的data
有了这几个属性,就好理解 this.queue
的作用了:当后面调用 this.queue.push
时,首先把 data
加入到队列,接着启动对列的消耗,这个过程就是每次从队列开头 shift
一项,然后调用 worker
函数,并传递刚刚从队列 shift
下列的 data
;
而在 WorkerPool
中,worker
就是 this.distributeJob
这个方法,至于 push
动作则是在得到 WorkerPool
实例后,调用实例的 run
方法中发生的,也就前面 pitch
方法的最后一步
workerPool.run(data, cb)
。
六、总结
本文介绍了 thread-loader
的 pitch
方法实现的具体逻辑:
- 使用
loaderUtils
获取thread-loader
在webpack.config.js
中的配置options
; - 调用
getPool
方法获取WorkerPool
实例;这个过程中会用到neo-async
的queue
,并且介绍了queue
的作用和运行机制; - 调用
this.async()
将thread-loader
变成一个异步loader
; - 调用
workerPool.run()
方法并传入包含 除thread-loader
之外的其他loader
的data
对象和添加依赖的cb
回调;
下一篇讨论 workerPool.run
方法,这个方法是 thread-loader
的入口方法,这里就要用到上面花了大量篇幅说用的 asyncQueue
。