在 Webpack 构建项目时,会碰到超多异步任务。像读取模块文件、用 Loader 转译代码、解析依赖关系,还有生成 Chunk 这些操作,要是一股脑全并发执行,那可就乱套了,比如资源抢着用、重复编译等问题都会冒出来。而 AsyncQueue
作为 Webpack 自带的异步任务调度器,就专门用来解决这些麻烦事儿,堪称 Webpack 构建流程里的核心模块。这篇文章,咱们就钻进源码里,把 AsyncQueue
的设计思路、核心代码实现,还有实际咋用都拆开来讲,带你搞明白 Webpack 是咋把异步任务管理得明明白白的。
一、AsyncQueue 简介
AsyncQueue
的代码在 Webpack 源码的 lib/util/AsyncQueue.js
里,它是 Webpack 构建流程 "任务调度中心"。它主要干三件事情:
-
让异步任务排队执行:保证任务按顺序一个个来,避免出现同一个模块反复编译,或者文件读写冲突这类并发问题;
-
可以设置任务优先级:能按照任务的重要程度分配优先级,比如先处理入口模块的依赖,后处理普通依赖,这样能加快构建速度;
-
隔离错误还能控制状态:就算某个任务执行失败了,也不会影响整个队列的运行,而且还支持暂停、恢复、清空队列这些操作,特别适合像热更新这种动态场景。
二、核心:构造函数
想要搞懂 AsyncQueue
,得先从它的构造函数入手。这个构造函数定义了队列的核心配置和状态管理方式,就像是整个模块的 "骨架"。
2.1 构造函数源码与参数解读
kotlin
// lib/util/AsyncQueue.js
class AsyncQueue {
/**
* @param {Object} options 队列配置项
* @param {Function} options.processor 任务处理器(必选):接收任务参数,返回 Promise
* @param {string} [options.name] 队列名称(可选):用于日志打印,默认 "unknown"
*/
constructor({ name, context, parallelism, parent, processor, getKey }) {
this._name = name; // 队列名称
this._context = context || "normal"; // 执行上下文
this._parallelism = parallelism || 1; // 并行处理数
this._processor = processor; // 处理函数
this._getKey =
getKey ||
/** @type {getKey<T, K>} */ ((item) => /** @type {T & K} */ (item)); // 获取队列条目键
/** @type {Map<K, AsyncQueueEntry<T, K, R>>} */
this._entries = new Map(); // 队列条目
/** @type {ArrayQueue<AsyncQueueEntry<T, K, R>>} */
this._queued = new ArrayQueue(); // 队列
/** @type {AsyncQueue<T, K, R>[] | undefined} */
this._children = undefined; // 子队列
this._activeTasks = 0; // 正在进行的任务数
/** @type {boolean} */
this._willEnsureProcessing = false; // 是否会确保处理
/** @type {boolean} */
this._needProcessing = false; // 是否需要处理
/** @type {boolean} */
this._stopped = false; // 是否停止
/** @type {AsyncQueue<T, K, R>} */
this._root = parent ? parent._root : this; // 根队列
if (parent) { // 父队列
if (this._root._children === undefined) {
this._root._children = [this];
} else {
this._root._children.push(this);
}
} // 子队列
this.hooks = { // 钩子
/** @type {AsyncSeriesHook<[T]>} */
beforeAdd: new AsyncSeriesHook(["item"]),
/** @type {SyncHook<[T]>} */
added: new SyncHook(["item"]),
/** @type {AsyncSeriesHook<[T]>} */
beforeStart: new AsyncSeriesHook(["item"]), // 开始处理前
/** @type {SyncHook<[T]>} */
started: new SyncHook(["item"]), // 开始处理后
/** @type {SyncHook<[T, WebpackError | null | undefined, R | null | undefined]>} */
result: new SyncHook(["item", "error", "result"]) // 处理结果
};
this._ensureProcessing = this._ensureProcessing.bind(this); // 确保处理
}
}
2.2 关键设计亮点
- 配置灵活 :
processor
、getKey
这些核心功能都是从外部传进来的,这样AsyncQueue
就不用管具体业务,比如模块编译、依赖解析这些细节,成了一个通用的调度器; - 状态管理清晰 :用
_running
、_stopped
这些状态变量,把队列的各个阶段分得明明白白,再也不用担心状态乱套; - Promise 封装方便 :
_promise
把队列整体的执行状态暴露出来,外面调用的时候,直接用await asyncQueue.add(task)
就能等着队列执行完,和异步构建流程配合得特别好。
三、核心方法解析:任务生命周期的实现
AsyncQueue
的核心在 "添加任务 - 执行任务 - 状态控制" 这三类方法里,这些方法管着任务从进队列到执行完的整个过程。
- 第一步:把任务丢进队列(add 方法)
js
/**
* @param {T} item an item
* @param {Callback<R>} callback callback function
* @returns {void}
*/
add(item, callback) { // 添加项
if (this._stopped) return callback(new WebpackError("Queue was stopped"));
this.hooks.beforeAdd.callAsync(item, (err) => { // 开始处理前
if (err) {
callback(
makeWebpackError(err, `AsyncQueue(${this._name}).hooks.beforeAdd`)
);
return;
}
const key = this._getKey(item);
const entry = this._entries.get(key); // 获取项
if (entry !== undefined) { // 任务已存在
if (entry.state === DONE_STATE) { // 任务已完成
if (inHandleResult++ > 3) { // 在处理结果的任务数超过3
process.nextTick(() => callback(entry.error, entry.result)); // 在下一个事件循环中调用回调函数
} else {
callback(entry.error, entry.result); // 调用回调函数
}
inHandleResult--;
} else if (entry.callbacks === undefined) {
entry.callbacks = [callback];
} else {
entry.callbacks.push(callback);
}
return;
}
const newEntry = new AsyncQueueEntry(item, callback);
if (this._stopped) { // 已停止
this.hooks.added.call(item);
this._root._activeTasks++;
process.nextTick(() =>
this._handleResult(newEntry, new WebpackError("Queue was stopped")) // 在下一个事件循环中处理结果
);
} else {
this._entries.set(key, newEntry); // 添加项
this._queued.enqueue(newEntry); // 入队
const root = this._root;
root._needProcessing = true;
if (root._willEnsureProcessing === false) { // 未确保处理
root._willEnsureProcessing = true;
setImmediate(root._ensureProcessing); // 确保处理
}
this.hooks.added.call(item); // 添加项
}
});
}
你想让 Webpack 处理一个模块,就会调用 AsyncQueue 的 add 方法。这一步里,AsyncQueue 主要逻辑是:
(1)先 检查:任务能不能加进来?
加任务前,会先触发 beforeAdd 钩子 ------ 相当于 "安检",比如检查任务是不是合法的。要是检查没过(比如钩子抛了错),直接给回调返回错误,任务就加不进来了。
(2)查 "重复":这任务是不是已经有了?
接着会用 getKey 给任务算个 "唯一标识"(比如模块 ID),然后查 _entries 这个 Map存着所有已经加进来的任务。
- 要是任务已经处理完了(DONE_STATE),直接把之前的结果回调给新请求,不用再处理一遍;
- 要是还在处理中或排队中,就把新的回调加到任务的 callbacks 里,等任务完成一起通知;
- 要是真的是新任务,就包成 AsyncQueueEntry,丢进 _queued 队列里。
(3)通知root队列
加完任务后,会告诉根队列(_root):"有新任务了,该处理了"。根队列会用 setImmediate 调用 _ensureProcessing 方法 ------ 这方法就是确保任务能被及时处理。
- 第二步:安排任务执行(_ensureProcessing 方法)
js
/**
* @returns {void}
*/
_ensureProcessing() { // 确保处理
while (this._activeTasks < this._parallelism) {
const entry = this._queued.dequeue();
if (entry === undefined) break;
this._activeTasks++;
entry.state = PROCESSING_STATE;
this._startProcessing(entry);
}
this._willEnsureProcessing = false;
if (this._queued.length > 0) return;
if (this._children !== undefined) {
for (const child of this._children) {
while (this._activeTasks < this._parallelism) {
const entry = child._queued.dequeue();
if (entry === undefined) break;
this._activeTasks++;
entry.state = PROCESSING_STATE;
child._startProcessing(entry);
}
if (child._queued.length > 0) return;
}
}
if (!this._willEnsureProcessing) this._needProcessing = false;
}
(1)先看有没有超过最大并行数
首先看当前正在处理的任务数(_activeTasks)有没有超过并行数(_parallelism)------ 比如并行数设为 2,现在只在处理 1 个任务,那就还能再安排一个。
(2)先处理自己的任务,再处理子任务
- 先从自己的 _queued 队列里拿任务,拿一个就标记为 PROCESSING_STATE,然后调用 _startProcessing 方法开始处理;
- 要是自己的任务处理完了,就看看有没有子队列(_children)------ 要是子队列有任务,就帮着处理子队列的任务(这就是父子队列的作用,父队列优先级高,还能共享并行数)。
(3)没任务了就停止
要是自己和子队列都没任务了,就把 _needProcessing 设为 false,相当于 "没事干了,等新任务再来"。
- 第三步:真正执行任务(_startProcessing 方法)
js
/**
* @param {AsyncQueueEntry<T, K, R>} entry the entry
* @returns {void}
*/
_startProcessing(entry) { // 开始处理
this.hooks.beforeStart.callAsync(entry.item, (err) => {
if (err) {
this._handleResult(
entry,
makeWebpackError(err, `AsyncQueue(${this._name}).hooks.beforeStart`)
);
return;
}
let inCallback = false;
try {
this._processor(entry.item, (e, r) => {
inCallback = true;
this._handleResult(entry, e, r);
});
} catch (err) {
if (inCallback) throw err;
this._handleResult(entry, /** @type {WebpackError} */ (err), null);
}
this.hooks.started.call(entry.item);
});
}
这一步就是调用你传进来的 processor 方法(比如编译模块的逻辑):
(1)执行 beforeStart 钩子
开始处理前,会触发 beforeStart 钩子 ------ 比如处理任务前先做些准备工作(比如检查依赖)。要是钩子出错了,直接调用 _handleResult 处理错误,任务就不执行了。
(2)执行任务,处理异常
然后调用 processor 执行任务,同时传一个回调函数 ------ 任务完成后会调用这个回调,把结果或错误传进去。
这里还有个小细节:用 try-catch 包着 processor 的调用,防止 processor 同步抛错。要是抛错了,也会调用 _handleResult 处理。
(3)"打卡":触发 started 钩子
任务开始执行后,会触发 started 钩子 ------ 相当于 "工人打卡,开始干活了",方便后续监控。
- 第四步:处理任务结果(_handleResult 方法)
js
/**
* @param {AsyncQueueEntry<T, K, R>} entry the entry
* @param {(WebpackError | null)=} err error, if any
* @param {(R | null)=} result result, if any
* @returns {void}
*/
_handleResult(entry, err, result) { // 处理结果
this.hooks.result.callAsync(entry.item, err, result, (hookError) => {
const error = hookError
? makeWebpackError(hookError, `AsyncQueue(${this._name}).hooks.result`)
: err;
const callback = /** @type {Callback<R>} */ (entry.callback);
const callbacks = entry.callbacks;
entry.state = DONE_STATE;
entry.callback = undefined;
entry.callbacks = undefined;
entry.result = result;
entry.error = error;
const root = this._root;
root._activeTasks--;
if (root._willEnsureProcessing === false && root._needProcessing) {
root._willEnsureProcessing = true;
setImmediate(root._ensureProcessing);
}
if (inHandleResult++ > 3) {
process.nextTick(() => {
callback(error, result);
if (callbacks !== undefined) {
for (const callback of callbacks) {
callback(error, result);
}
}
});
} else {
callback(error, result);
if (callbacks !== undefined) {
for (const callback of callbacks) {
callback(error, result);
}
}
}
inHandleResult--;
});
}
任务执行完了,不管成功还是失败,都要走这一步:
(1)先 "收尾":执行 result 钩子
首先触发 result 钩子 ------ 比如任务完成后做些清理工作(比如释放资源)。要是钩子出错了,会把钩子的错误和任务本身的错误合并。
(2)记录结果,通知等待的人
- 把任务的状态设为 DONE_STATE,记录结果(result)和错误(error);
- 然后调用任务本身的 callback,再遍历 callbacks 数组,把结果或错误通知所有等待的人;
- 这里还有个小优化:要是回调嵌套太多(inHandleResult 超过 3),就用 process.nextTick 延迟执行,避免栈溢出。
(3)继续处理下一个任务
最后把 _activeTasks 减 1,然后再调用 _ensureProcessing------ 相当于 "这个任务干完了,再看看有没有别的任务要干"。
- 其他实用功能:应急和监控
(1)stop 方法:紧急停止
js
stop() { // 停止
this._stopped = true; // 已停止
const queue = this._queued;
this._queued = new ArrayQueue();
const root = this._root;
for (const entry of queue) { // 遍历队列
this._entries.delete(
this._getKey(/** @type {AsyncQueueEntry<T, K, R>} */ (entry).item)
); // 删除任务
root._activeTasks++; // 活动任务数增加
this._handleResult(
/** @type {AsyncQueueEntry<T, K, R>} */ (entry),
new WebpackError("Queue was stopped")
); // 处理结果
}
}
要是遇到致命错误,想停止所有任务,就调用 stop 方法 ------ 会把队列里的任务都清掉,给每个任务返回 "Queue was stopped" 错误,相当于 "紧急停止,所有任务都不做了"。
(2)invalidate 方法:
js
/**
* @param {T} item an item
* @returns {void}
*/
invalidate(item) { // 无效项
const key = this._getKey(item);
const entry =
/** @type {AsyncQueueEntry<T, K, R>} */
(this._entries.get(key));
this._entries.delete(key);
if (entry.state === QUEUED_STATE) {
this._queued.delete(entry);
}
}
比如某个模块的代码改了,之前的编译任务就无效了,调用 invalidate 方法 ------ 会把任务从 _entries 和 _queued 里删掉,相当于 "这个任务不算数了,别处理了"。
(3)isProcessing/isQueued/isDone:"查任务状态"
js
/**
* @param {T} item an item
* @returns {boolean} true, if the item is currently being processed
*/
isProcessing(item) { // 正在处理项
const key = this._getKey(item);
const entry = this._entries.get(key);
return entry !== undefined && entry.state === PROCESSING_STATE;
}
/**
* @param {T} item an item
* @returns {boolean} true, if the item is currently queued
*/
isQueued(item) {
const key = this._getKey(item);
const entry = this._entries.get(key);
return entry !== undefined && entry.state === QUEUED_STATE;
}
/**
* @param {T} item an item
* @returns {boolean} true, if the item is currently queued
*/
isDone(item) { // 完成项
const key = this._getKey(item);
const entry = this._entries.get(key);
return entry !== undefined && entry.state === DONE_STATE;
}
这些方法就是查任务当前的状态 ------ 比如 isProcessing 看任务是不是正在处理中,方便做监控或判断逻辑。