jQuery的ready事件是前端开发中最常用的功能之一,绝大多数人只是简单地使用$(document).ready()等待页面加载完成。但如果页面初始化依赖于某些异步资源,比如动态加载的脚本、AJAX获取的配置、或者模块加载器尚未完成初始化,过早触发ready就会导致代码执行时报错。jQuery内部提供了一个名为readyWait的计数器,专门用来解决这种“依赖未就绪”的问题。

readyWait在jQuery源码中的角色
要理解readyWait,需要先了解jQuery对DOMContentLoaded事件的处理流程。在jQuery 3.x中,ready机制的核心是jQuery.ready.promise,它返回一个Promise对象,并在DOM加载完成时resolve。与原生DOMContentLoaded不同,jQuery的ready可以多次绑定回调,所有回调会依次执行。在触发所有回调之前,jQuery会检查readyWait的值,只有当readyWait为0时才会真正执行回调队列。
具体来说,jQuery内部维护了一个名为readyWait的私有变量,初始值为0。jQuery提供了两个公开方法:$.holdReady(true)会将readyWait加1,$.holdReady(false)会将其减1。当readyWait大于0时,即使DOMContentLoaded已经触发,jQuery也会把ready回调的触发延迟,直到readyWait重新变为0。这个机制在源码中通过一个名为jQuery.ready的函数来实现,该函数内部会判断readyWait是否大于0,如果是则直接返回,否则才真正触发回调。
// jQuery 源码简化版
var readyWait = 0;
function holdReady(hold) {
if (hold) {
readyWait++;
} else {
readyWait--;
}
}
function ready() {
if (readyWait > 0) {
return;
}
// 执行所有通过 $(document).ready() 注册的回调
jQuery.readyList.resolveWith(document, [jQuery]);
}
从上面的简化代码可以看出,readyWait是一个阻塞阀。即使DOM已经加载完成,只要之前调用了holdReady(true),ready函数就会提前退出,等待后续的holdReady(false)调用。这种设计使得开发者可以精准控制ready回调的执行时机,非常适合在依赖异步资源加载的场景中使用。
实际场景:动态脚本加载与模块依赖
假设页面需要先加载一个第三方数据分析脚本,并且该脚本会向window对象注入一个全局API。如果直接在DOM ready回调中使用这个API,很可能在脚本尚未下载并执行完毕时就报错。借助readyWait,可以先把ready回调挂起,等脚本加载完成后再释放。
// 页面初始化时立即挂起 ready
$.holdReady(true);
// 动态加载第三方脚本
var script = document.createElement('script');
script.src = 'https://cdn.ippipp.com/analytics.js';
script.onload = function() {
// 脚本加载完成,释放 ready
$.holdReady(false);
};
script.onerror = function() {
// 加载失败也要释放,避免死锁
$.holdReady(false);
};
document.head.appendChild(script);
$(document).ready(function() {
// 此时 analytics 全局对象一定存在
analytics.init();
});
这个例子展示了readyWait最典型的用法:先调用holdReady(true)阻止ready触发,然后在异步操作完成后的回调中调用holdReady(false)恢复触发。需要注意的是,holdReady的配对必须严格,否则会导致ready永远不触发。即使在加载失败的情况下,也要记得释放,否则页面将无响应。
除了动态脚本,还有更复杂的场景:比如使用AMD或CommonJS模块加载器时,jQuery ready的回调可能需要在某个模块初始化完成后才能执行。通过readyWait,开发者可以将这个控制权从框架内部转移到业务代码中,实现更灵活的依赖管理。
深入源码:holdReady与ready.promise的协同工作
在jQuery 3.x中,holdReady方法定义在jQuery对象上,它真正操作的就是闭包内的readyWait变量。当调用$.holdReady(true)时,readyWait加1;调用$.holdReady(false)时,readyWait减1。而jQuery.ready.promise内部会监听DOMContentLoaded事件,当事件触发后,会调用一个名为completed的函数,该函数会调用jQuery.ready。在jQuery.ready中,第一行就是检查readyWait是否大于0,如果是则直接返回,否则才调用resolveWith触发所有注册的回调。
// 简化版 ready.promise 实现
jQuery.ready = function(wait) {
// 如果 readyWait 大于 0,说明还有依赖未完成
if (wait === true ? --jQuery.readyWait : jQuery.isReady) {
return;
}
jQuery.isReady = true;
// 触发所有 ready 回调
readyList.resolveWith(document, [jQuery]);
};
jQuery.holdReady = function(hold) {
if (hold) {
jQuery.readyWait++;
} else {
jQuery.ready(true);
}
};
注意上面代码中jQuery.ready(true)的调用:当传入true时,它会把readyWait减1,然后继续判断是否已经准备好。如果readyWait降为0且DOM已经加载完成,就会执行回调;如果readyWait仍然大于0,则继续等待。这种设计保证了多次holdReady(true)只需要对应次数的holdReady(false)就能正确触发ready。
还有一个细节:ready.promise内部会先判断document.readyState,如果已经是complete,则会立即调用jQuery.ready,但同样会受到readyWait的阻塞。因此哪怕页面加载速度极快,在脚本执行时DOM已经完备,只要之前调用了holdReady(true),ready回调仍然会被延迟。
实际应用中的坑与替代方案
使用readyWait时最常见的错误是忘记配套调用holdReady(false)。如果只调用了holdReady(true)而没有在后续调用holdReady(false),那么所有ready回调将永远不执行,页面看起来像是“卡死”了。此外,如果holdReady(false)比holdReady(true)多调用了一次,readyWait会变成负值,导致ready提前触发。因此建议在复杂的加载流程中,对holdReady的调用进行封装,确保每个异步分支都能正确释放。
另一个需要注意的点是,readyWait只影响jQuery的ready回调,不会影响原生DOMContentLoaded事件。如果项目同时使用了原生事件监听,它们仍会按时触发。如果希望统一控制,建议所有初始化代码都通过jQuery的ready机制执行,避免混用。
对于更现代的工程,如果不再依赖jQuery,可以使用原生的Promise.all来等待多个异步操作完成,然后再执行初始化代码。但readyWait作为jQuery历史遗留的优雅方案,在维护老项目时仍然值得理解。它的设计思路——用计数器延迟事件触发——也可以借鉴到其他框架或自定义事件系统中。