函数是否会因上下文切换未被调用?DOMContentLoaded绑定风险与解决方案
你提的这个问题非常好——这确实是一个存在的竞态条件,会导致doSomething()永远无法被执行,我来给你拆解清楚:
问题出在哪里?
先看你给出的代码:
(function(fn){ var d=document; (d.readyState=='loading') ? d.addEventListener('DOMContentLoaded',fn) : fn(); })(function(){ doSomething(); });
这段代码的逻辑是先判断文档是否还在加载(readyState === 'loading'),如果是就绑定DOMContentLoaded事件,否则直接执行回调。
但问题在于:判断readyState和绑定事件这两个操作之间存在一个微小的时间窗口。假设在你判断完readyState === 'loading'之后,浏览器的事件循环刚好处理了DOM加载完成的任务,把DOMContentLoaded事件触发了,然后才回到你的代码执行addEventListener。这时候事件已经触发过了,新绑定的回调函数根本不会被调用,最终doSomething()就彻底没机会运行了。
为什么会这样?因为JavaScript是单线程的,但浏览器的事件处理是异步的——当DOM构建完成时,DOMContentLoaded事件会被加入任务队列,等待主线程空闲时执行。如果你的代码在判断状态后刚好被打断,事件先执行了,后续绑定的监听就无效了。
正确的解决方案
要解决这个竞态条件,核心思路是先绑定事件监听,再检查当前状态,确保无论事件是否已经触发,回调都能被执行。这里有两种稳妥的写法:
写法一:先绑定事件,再检查状态
(function(fn) { const d = document; // 先绑定事件监听 d.addEventListener('DOMContentLoaded', fn); // 如果文档已经加载完成(readyState不是loading),直接执行回调 if (d.readyState !== 'loading') { fn(); } })(function() { doSomething(); });
这种写法的逻辑是:
- 如果文档还在加载中,事件触发时会执行
fn; - 如果文档已经加载完成,绑定事件后直接手动执行
fn(因为事件已经触发过,绑定的监听不会再执行,所以不会重复调用)。
写法二:使用一次性事件处理函数(更严谨)
如果担心极端情况下重复执行(虽然概率极低),可以用一次性的处理函数,执行后移除监听:
(function(fn) { const d = document; function handleReady() { fn(); // 执行后移除事件监听,避免重复触发 d.removeEventListener('DOMContentLoaded', handleReady); } d.addEventListener('DOMContentLoaded', handleReady); // 如果文档已加载,直接调用处理函数 if (d.readyState !== 'loading') { handleReady(); } })(function() { doSomething(); });
这种写法能确保fn只会被执行一次,无论事件是在绑定前还是绑定后触发。
总结
你的初始代码确实存在竞态条件的风险,调整绑定事件和状态检查的顺序就能解决问题——先把事件监听挂上,再根据当前状态决定是否立即执行回调,这样就不会错过事件触发的时机了。
内容的提问来源于stack exchange,提问作者wp42

