深入剖析 Webpack AsyncQueue 源码:异步任务调度的核心

在 Webpack 构建项目时,会碰到超多异步任务。像读取模块文件、用 Loader 转译代码、解析依赖关系,还有生成 Chunk 这些操作,要是一股脑全并发执行,那可就乱套了,比如资源抢着用、重复编译等问题都会冒出来。而 AsyncQueue 作为 Webpack 自带的异步任务调度器,就专门用来解决这些麻烦事儿,堪称 Webpack 构建流程里的核心模块。这篇文章,咱们就钻进源码里,把 AsyncQueue 的设计思路、核心代码实现,还有实际咋用都拆开来讲,带你搞明白 Webpack 是咋把异步任务管理得明明白白的。

一、AsyncQueue 简介

AsyncQueue 的代码在 Webpack 源码的 lib/util/AsyncQueue.js 里,它是 Webpack 构建流程 "任务调度中心"。它主要干三件事情:

  1. 让异步任务排队执行:保证任务按顺序一个个来,避免出现同一个模块反复编译,或者文件读写冲突这类并发问题;

  2. 可以设置任务优先级:能按照任务的重要程度分配优先级,比如先处理入口模块的依赖,后处理普通依赖,这样能加快构建速度;

  3. 隔离错误还能控制状态:就算某个任务执行失败了,也不会影响整个队列的运行,而且还支持暂停、恢复、清空队列这些操作,特别适合像热更新这种动态场景。

二、核心:构造函数

想要搞懂 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 关键设计亮点

  • 配置灵活processorgetKey 这些核心功能都是从外部传进来的,这样 AsyncQueue 就不用管具体业务,比如模块编译、依赖解析这些细节,成了一个通用的调度器;
  • 状态管理清晰 :用 _running_stopped 这些状态变量,把队列的各个阶段分得明明白白,再也不用担心状态乱套;
  • Promise 封装方便_promise 把队列整体的执行状态暴露出来,外面调用的时候,直接用 await asyncQueue.add(task) 就能等着队列执行完,和异步构建流程配合得特别好。

三、核心方法解析:任务生命周期的实现

AsyncQueue 的核心在 "添加任务 - 执行任务 - 状态控制" 这三类方法里,这些方法管着任务从进队列到执行完的整个过程。

  1. 第一步:把任务丢进队列(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 方法 ------ 这方法就是确保任务能被及时处理。

  1. 第二步:安排任务执行(_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,相当于 "没事干了,等新任务再来"。​

  1. 第三步:真正执行任务(_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 钩子 ------ 相当于 "工人打卡,开始干活了",方便后续监控。​

  1. 第四步:处理任务结果(_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. 其他实用功能:应急和监控

(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 看任务是不是正在处理中,方便做监控或判断逻辑。

相关推荐
JarvanMo15 小时前
Flutter 的 Hero Widget 有一个隐藏的超能力(大多数开发者从未使用过)
前端
WindrunnerMax15 小时前
从零实现富文本编辑器#7-基于组合事件的半受控输入模式
前端·前端框架·github
Cache技术分享15 小时前
177. Java 注释 - 重复注释
前端·后端
rocksun15 小时前
如何使用Enhance构建App:后端优先框架指南
前端·前端框架·前端工程化
Mike的AI工坊15 小时前
[知识点记录]createWebHistory的用法
前端
红色石头本尊15 小时前
8-Gsap动画库基本使用与原理
前端
小高00716 小时前
🎯v-for 先还是 v-if 先?Vue2/3 编译真相
前端·javascript·vue.js
原生高钙16 小时前
大模型的流式响应实现
前端·ai编程
zzywxc78716 小时前
如何利用AI IDE快速构建一个简易留言板系统
开发语言·前端·javascript·ide·vue.js·人工智能·前端框架