持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的8天,点击查看活动详情
一、前情回顾
上一篇小作文文介绍 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对象、添加依赖(addDependency)的cb回调;
本篇的主题则是看看 workerPool.run(data, cb) 方法都做了哪些工作,该方法来自 WorkerPool.prototype.run。
二、WorkerPoll.prototype.run
- 方法位置:
src/WorkerPool.js - 方法参数:
- data: 要加入到 poolQueue(即上文 neo-async/queue.js 创建的队列)中的数据,下称 data
- callback:poolQueue 的 worker 方法执行结束后要调用的回调方法,下称 cb。这个 cb 是接收 thread-loader 运行 loader 后的结果用的。
data = {
loaders: this.loader.slice(1, ....), // 除 thread-loader 之外的其他loader
resource: ...,
query: ...,
sourceMap: ...,
resourceQuery: ...
// ....
}
// cb
callback = (r) => {
// callback 是接收 thread-loader 并发运行 loader 后的结果用的
// 结果包含了不同类型的依赖,然后调用相应类型的添加依赖的方法添加依赖
if (r) {
f.fileDependency.forEach(d => this.addDependency(d));....
}
}
- 方法作用:
- 3.1 维护
this.activeJobs累加 - 3.2 向
workerPool.poolQueue中push数据
- 3.1 维护
WorkerPool {
constructor(options) {
// ...
this.poolQueue = asyncQueue(
this.distributeJob.bind (this), // poolQueue 的 worker 方法
options.poolParallelJobs // 并发数
);
// ....
}
distributeJob(data, callback) {}
// 这个 run 就是主角了
run(data, callback) {
if (this.timeout) {
clearTimeout(this.timeout);
this.timeout = null;
}
this.activeJobs += 1;
this.poolQueue.push(data, callback);
}
}
三、this.poolQueue.push 方法
上文中提到过 this.poolQueue 是 neo-async/queue.js 创建出来的,它是一个双向链表,并且实现了 push、unshift、shift 等操作方法。
neo-async/queue.js 创建出来的队列是个需要异步管控的队列,这个异步队列创建时接收处理对列中 data 的 worker(还接收一个最大并发数量的 concurency )。
当向异步队列(this.poolQueue)添加数据即 this.poolQueue.push 时,传递了 data 和一个 callback 函数,这个 callback 是最终接收 worker 处理 data 后的结果用的。
3.1 baseQueue 中的私有 push 方法
-
方法位置:
node_modules/neo-async/async.js -> function baseQueue -> q.push -
方法参数:
- 2.1
tasks: 从上面的workerPool.run的调用能够看出,tasks就是包含loaders的data对象 - 2.2
callback: 这个callback就是前面的cb,作用是接收worker处理data后返回的依赖。
- 2.1
-
方法作用:
push方法是_insert的科里化方法,为_insert的第三个参数绑定undefined(不给它传第三个参数就是绑定undefined)
// queue 就是 baseQueue 的科里化方法
function queue(worker, concurrency) {
return baseQueue(true, worker, concurrency);
}
// baseQueue
function baseQueue(isQueue, worker, concurrency, payload) {
var _callback, _unshift;
// 上面的 this.poolQueue 就是这个 q 对象
var q = {
push: push, // this.poolQueue.push 就是下面的 function push
process: isQueue ? runQueue : runCargo,
_worker: worker
};
return q;
// push 方法在这里
function push(tasks, callback) {
// 只给 _insert 传两个参数
// 而 _insert 本身是接收三个参数的
_insert(tasks, callback);
}
function _exec(task) { }
function _insert(tasks, callback, unshift) {}
function _next(q, tasks) { }
function runQueue() { }
}
3.2 baseQueue 私有方法 _insert
-
方法位置:
node_modules/neo-async/async.js -> function baseQueue -> function _insert -
方法参数:
- 2.1
tasks: 要添加到poolQueue的数据,也是需要被worker处理的任务,所以叫做tasks;它接收到就是上面的data对象 - 2.2
callback:回调函数,这个回调是接收tasks被处理后的结果的。在这里,就是前面的cb函数了。 - 2.3
unshift:向队列开头插入方法,这里是undefined
- 2.1
-
方法作用:
- 3.1 格式化
tasks参数,如果不是数组,将其包装成数组 最后赋值到_tasks变量 - 3.2 将
callback、unshift参数分别赋值到_callback、_unshift变量 - 3.3 调用
arrayEachSync方法遍历_tasks数组,这里面放的就是前面的data(这里有loaders) 对象,遍历时,调用的迭代方法是_exec方法
- 3.1 格式化
// queue 就是 baseQueue 的科里化方法
function queue(worker, concurrency) {
return baseQueue(true, worker, concurrency);
}
// baseQueue
function baseQueue(isQueue, worker, concurrency, payload) {
var _callback, _unshift;
// 上面的 this.poolQueue 就是这个 q 对象
var q = {
push: push, // this.poolQueue.push 就是下面的 function push
};
return q;
function push(tasks, callback) {
// _insert 是下面的 function _insert
_insert(tasks, callback);
}
function _insert(tasks, callback, unshift) {
// tasks 就是 data 对象: { loaders, resource, query ... }
// callback 就是上面的收集依赖的 cb 函数: (r) => { if (r) this.fileDependencies.forEach(d => this.addDependency(d)) }
q.started = true;
var _tasks = isArray(tasks) ? tasks : [tasks];
if (tasks === undefined || !_tasks.length) {
if (q.idle()) {
nextTick(q.drain);
}
return;
}
// push 科里化了 _insert,此时 unshift 参数是 undefined
_unshift = unshift;
// 把收集依赖的 callback 函数赋值到 _callback 遍历,注意有一个下划线
_callback = callback;
// _tasks -> [{ loaders, source, query }]
// _callback = (r) => ....this.addDependency(d)
// arrayEachSync: 遍历 _tasks 数组,其迭代函数时 _exec 方法
// 为 _tasks 每一项调用 _exec, 最后返回 _tasks
arrayEachSync(_tasks, _exec);
// 防止泄漏 _callback,要清除掉
_callback = undefined;
}
}
3.3 arrayEachSync 方法
-
方法位置:
node_modules/neo-async/async.js -> function arrayEachSync -
方法参数:
- 2.1
array: 需要被遍历的数组;上面_insert调用时传入的是_tasks数组,数组项是data对象; - 2.2
iterator:遍历时为数组每一项执行的迭代函数;上面_insert调用时传入的是_exec方法;
- 2.1
-
方法作用:遍历
array数组,调用iterator最后返回array。有点像Array.prototype.forEach,但是forEach没有返回值而已;
function arrayEachSync(array, iterator) {
var index = -1;
var size = array.length;
while (++index < size) {
iterator(array[index], index);
}
return array;
}
3.3 baseQueue 私有方法 _exec
-
方法位置:
node_modules/neo-async/async.js -> function baseQueue -> function _exec -
方法参数:
task,需要被处理的任务,在thread-loader中是一个data对象(包含 loaders) -
方法作用:
- 3.1 将
push接收到的data(被保证成_tasks数组) 和cb回调(赋值到_callback)格式化成统一的item对象,并加入到队列中;、 - 3.2 在下个事件循环中调用
q.process方法消耗队列。所谓消耗队列就是调用前面创建队列时传入的 worker处理队列中的对象(此时队列中的项形如{ data: task, callback: _callback },其中task是包含loaders的data对象,_callback就是接收worker处理task后的结果的cb` 回调)
- 3.1 将
function _exec(task) {
var item = {
data: task,
callback: _callback
};
if (_unshift) {
q._tasks.unshift(item);
} else {
q._tasks.push(item);
}
nextTick(q.process);
}
3.4 q.process 方法
-
方法位置:
node_modules/neo-async/async.js -> function baseQueue -> q.process -
方法参数:暂无
-
方法作用:根据
baseQueue的第一个参数是否为true,决定q.process表示不同的方法,这里isQueue为true,所以q.process代表的是runQueue方法,runQueue方法是消耗队列的。
// queue 就是 baseQueue 的科里化方法,
// 绑定 baseQueue 的第一个参数 isQueue 为 true
function queue(worker, concurrency) {
return baseQueue(true, worker, concurrency);
}
// baseQueue
function baseQueue(isQueue, worker, concurrency, payload) {
var q = {
process: isQueue ? runQueue : runCargo, // isQueue 是 queue 科里化绑定好的 true
_worker: worker // worker 就是创建 poolQueue 传入的 this.distributeJob 方法
};
return q;
// isQueue 为 true,q.process 就是下面的 runQueue
function runQueue() { }
}
3.5 baseQueue 私有方法 runQueue
-
方法位置:
node_modules/neo-async/async.js -> function baseQueue -> function runQueue -
方法参数:暂无
-
方法作用:这个
runQueue就是实现thread-loader并发的核心了,这里进行了并发控制;- 3.1 结合当前队列的状态
q.paused/并发数限制/队列不为空 等条件,条件成立则依次出队列(shift)取出任务task; - 3.2 创建上一个
worker结束后需要调用的done回调; - 3.3 将从队列中取出的调用创建队列时传入的
worker方法,在thread-loader的workerPool的poolQueue中,worker就是this.distribute.bind(this)即WorkerPool.prototype.distribute方法;
- 3.1 结合当前队列的状态
function runQueue() {
while (!q.paused && workers < q.concurrency && q._tasks.length) {
var task = q._tasks.shift();
workers++;
workersList.push(task);
if (q._tasks.length === 0) {
q.empty();
}
if (workers === q.concurrency) {
q.saturated();
}
// 这个是传递给 worker
var done = _next(q, [task]);
// worker 就是 this.distribute.bind(this) 即 WorkerPool.prototype.distribute 方法
// 要记住 worker 收到的回调不是 cb,而是由 runQueue 创造出来的 done 方法
worker(task.data, done);
}
}
四、总结
本篇小作文介绍了 thread-loader 的入口方法 WorkerPool.prototype.run 方法的内部逻辑,重点介绍了 this.poolQueue.push 方法从接收数据(data, cb),一直到最后 neo-async 调用声明队列时传入的 worker 启动对 data 的处理的全过程。
这个部分涉及到 neo-async 的 queue 中的逻辑,这个逻辑是控制并发的核心,最后要强调几点:
-
workerPool.run(data, cb)传递的第一个参数包含除thread-loader以外的loaders,我们称之为data对象,第二个参树是个回调,用于接收worker处理data后返回的loaders的执行结果的回调函数,称为cb; -
workerPool是调用asyncQueue方法得到的异步队列,它的worker是this.distribute方法; -
在
neo-async最后调用worker时,在poolQueue中worker即this.distriburte,它接收的第二个参数不是前面的cb,而是runQueue创造出来的done方法