Dan*_*Lew 833 javascript dom event-loop
我最近遇到了一个相当讨厌的错误,其中代码是<select>
通过JavaScript动态加载的.这种动态加载<select>
具有预先选择的值.在IE6中,我们已经有了修复所选内容的代码<option>
,因为有时它<select>
的selectedIndex
值与所选<option>
的index
属性不同步,如下所示:
field.selectedIndex = element.index;
Run Code Online (Sandbox Code Playgroud)
但是,此代码无效.即使selectedIndex
正确设置了字段,最终也会选择错误的索引.但是,如果我alert()
在正确的时间插入声明,则会选择正确的选项.考虑到这可能是某种时间问题,我尝试了一些随机的东西,我之前在代码中看到过:
var wrapFn = (function() {
var myField = field;
var myElement = element;
return function() {
myField.selectedIndex = myElement.index;
}
})();
setTimeout(wrapFn, 0);
Run Code Online (Sandbox Code Playgroud)
这有效!
我已经找到了解决问题的方法,但是我很不安,因为我不知道为什么这会解决我的问题.有人有官方解释吗?使用"稍后"调用我的功能可以避免哪些浏览器问题setTimeout()
?
sta*_*san 787
这是有效的,因为你正在进行合作多任务.
浏览器必须同时执行许多操作,其中只有一个是执行JavaScript.但JavaScript经常用于的一件事是要求浏览器构建一个显示元素.这通常被认为是同步完成的(特别是当JavaScript不是并行执行时),但不能保证是这种情况,并且JavaScript没有明确定义的等待机制.
解决方案是"暂停"JavaScript执行以让渲染线程赶上来.这是setTimeout()
超时为0的效果.它就像C中的一个线程/进程产量.虽然它似乎说"立即运行"但它实际上让浏览器有机会完成一些非JavaScript事情,这些事情一直在等待完成这一新的JavaScript之前完成.
(实际上,setTimeout()
在执行队列的末尾重新排队新的JavaScript.请参阅注释以获取更长解释的链接.)
IE6恰好更容易出现此错误,但我已经看到它出现在旧版本的Mozilla和Firefox中.
请参阅Philip Roberts的演讲"事件循环到底是什么?" 有更详尽的解释.
DVK*_*DVK 638
前言:
重要提示:虽然它最受欢迎和接受,但@staticsan接受的答案实际上并不正确! - 请参阅David Mulder对于解释原因的评论.
其他一些答案是正确的,但实际上没有说明要解决的问题是什么,所以我创建了这个答案,以提供详细的说明.
因此,我将详细介绍浏览器的功能以及如何使用setTimeout()
帮助.它看起来很长,但实际上非常简单明了 - 我只是非常详细.
更新:我已经制作了一个JSFiddle来演示以下解释:http://jsfiddle.net/C2YBE/31/.非常感谢 @ThangChung帮助启动它.
更新2:为了防止JSFiddle网站死亡或删除代码,我在最后添加了代码到这个答案.
细节:
想象一下带有"做某事"按钮和结果div的网络应用程序.
onClick
"do something"按钮的处理程序调用函数"LongCalc()",它执行两项操作:
做了很长的计算(比如需要3分钟)
将计算结果打印到结果div中.
现在,你的用户开始测试这个,点击"做某事"按钮,页面就在那里做3分钟看似没事,他们变得焦躁不安,再次点击按钮,等待1分钟,没有任何反应,再次点击按钮......
问题很明显 - 你想要一个"状态"DIV,它显示了正在发生的事情.让我们看看它是如何工作的.
所以你添加一个"Status"DIV(最初为空),并修改onclick
处理程序(函数LongCalc()
)来做4件事:
将状态"计算...可能需要约3分钟"填充到状态DIV中
做了很长的计算(比如需要3分钟)
将计算结果打印到结果div中.
将"已完成计算"状态填充到状态DIV中
并且,您乐意将应用程序提供给用户重新测试.
他们回到你身边看起来很生气.并解释当他们点击按钮时,状态DIV永远不会更新"计算..."状态!
你挠头,在StackOverflow(或阅读文档或谷歌)上四处询问,并意识到问题:
浏览器将事件产生的所有"TODO"任务(UI任务和JavaScript命令)放入单个队列中.不幸的是,使用新的"Calculating ..."值重新绘制"Status"DIV是一个单独的TODO,它会一直到队列的末尾!
以下是用户测试期间事件的细分,每个事件后队列的内容:
[Empty]
[Execute OnClick handler(lines 1-4)]
[Execute OnClick handler(lines 2-4), re-draw Status DIV with new "Calculating" value]
.请注意,当DOM更改瞬间发生时,要重新绘制相应的DOM元素,您需要一个由DOM更改触发的新事件,该事件在队列末尾.[Execute OnClick handler(lines 3-4), re-draw Status DIV with "Calculating" value]
.[Execute OnClick handler(line 4), re-draw Status DIV with "Calculating" value, re-draw result DIV with result]
.[Execute OnClick handler, re-draw Status DIV with "Calculating" value, re-draw result DIV with result; re-draw Status DIV with "DONE" value]
.return
从onclick
处理程序子执行隐含.我们从队列中取出"Execute OnClick handler"并开始执行队列中的下一个项目.因此,潜在的问题是"状态"DIV的重新绘制事件在结束时被放置在队列中,在"执行第2行"事件之后需要3分钟,因此实际的重新绘制直到计算完成后.
救援来了setTimeout()
.它有什么用?因为通过调用长执行代码setTimeout
,实际上创建了2个事件:setTimeout
执行本身,和(由于0超时),正在执行的代码的单独队列条目.
因此,为了解决您的问题,您将onClick
处理程序修改为TWO语句(在新函数中或仅在其中的块中onClick
):
将状态"计算...可能需要约3分钟"填充到状态DIV中
执行setTimeout()
0超时和LongCalc()
函数调用.
LongCalc()
功能与上次几乎相同,但显然没有"计算..."状态DIV更新为第一步; 而是立即开始计算.
那么,事件序列和队列现在看起来像什么?
[Empty]
[Execute OnClick handler(status update, setTimeout() call)]
[Execute OnClick handler(which is a setTimeout call), re-draw Status DIV with new "Calculating" value]
.[re-draw Status DIV with "Calculating" value]
.队列中没有任何新内容,持续0秒.[re-draw Status DIV with "Calculating" value, execute LongCalc (lines 1-3)]
.[execute LongCalc (lines 1-3)]
.请注意,此重新绘制事件可能实际发生在闹钟响起之前,这也适用.万岁!在计算开始之前,状态DIV刚刚更新为"计算..."!
下面是来自JSFiddle的示例代码,说明了这些示例:http://jsfiddle.net/C2YBE/31/:
HTML代码:
<table border=1>
<tr><td><button id='do'>Do long calc - bad status!</button></td>
<td><div id='status'>Not Calculating yet.</div></td>
</tr>
<tr><td><button id='do_ok'>Do long calc - good status!</button></td>
<td><div id='status_ok'>Not Calculating yet.</div></td>
</tr>
</table>
Run Code Online (Sandbox Code Playgroud)
JavaScript代码:(执行时onDomReady
可能需要jQuery 1.9)
function long_running(status_div) {
var result = 0;
// Use 1000/700/300 limits in Chrome,
// 300/100/100 in IE8,
// 1000/500/200 in FireFox
// I have no idea why identical runtimes fail on diff browsers.
for (var i = 0; i < 1000; i++) {
for (var j = 0; j < 700; j++) {
for (var k = 0; k < 300; k++) {
result = result + i + j + k;
}
}
}
$(status_div).text('calculation done');
}
// Assign events to buttons
$('#do').on('click', function () {
$('#status').text('calculating....');
long_running('#status');
});
$('#do_ok').on('click', function () {
$('#status_ok').text('calculating....');
// This works on IE8. Works in Chrome
// Does NOT work in FireFox 25 with timeout =0 or =1
// DOES work in FF if you change timeout from 0 to 500
window.setTimeout(function (){ long_running('#status_ok') }, 0);
});
Run Code Online (Sandbox Code Playgroud)
ade*_*hox 21
如果您不想观看整个视频,这里有一个需要理解的简单解释,以便能够理解这个问题的答案:
从现在开始,我们正在讨论“浏览器中”的 JavaScript。像这样的东西setTimeout
确实是浏览器的东西,而不是 JavaScript 本身的一部分。
现在我们将另一个空间称为第二空间。
fn
是一个函数。这里要理解的重要一点是fn();
call 不等于setTimeout(fn, 0);
call,这将在下面进一步解释。0
我们首先假设另一个延迟,而不是延迟,例如 5000 毫秒setTimeout(fn, 5000);
:需要注意的是,这仍然是一个“函数调用”,所以它必须放在主空间上,并在完成后从主空间中删除,但是等等!,我们不喜欢整个漫长而无聊的 5 秒延迟。这会阻塞主空间,并且不允许 JavaScript 同时运行任何其他东西。
值得庆幸的是,这并不是浏览器设计者设计它们的工作方式。相反,这个 call( setTimeout(fn, 5000);
) 是立即完成的。这一点非常重要:即使有 5000 毫秒的延迟,这个函数调用也是瞬间完成的!接下来会发生什么?它被从主空间中移除。它将被放置在哪里?(因为我们不想失去它)。您可能猜对了:浏览器听到这个调用并将其放在第二个空格上。
浏览器会跟踪 5 秒的延迟,一旦超过,它就会查看主空间,然后“当它为空时”,将回调放fn();
回到它上面。这就是setTimeout
工作原理。
所以,回到setTimeout(fn, 0)
,即使延迟为零,这仍然是对浏览器的调用,浏览器立即听到它并将其拾取,并将其放在第二个空间上,只有在以下情况下才将其放回到主空间上:主空间再次空了,而且并不是真正的 0 毫秒后。
我真的建议您也观看该视频,因为他解释得非常好,并且更开放了技术问题。
Arl*_*ley 20
大多数浏览器都有一个称为主线程的进程,它负责执行一些JavaScript任务,UI更新,例如:绘画,重绘或重排等.
一些JavaScript执行和UI更新任务排队到浏览器消息队列,然后被分派到浏览器主线程以执行.
在主线程忙时生成UI更新时,任务将添加到消息队列中.
setTimeout(fn, 0);
将此添加fn
到要执行的队列的末尾.它会在给定的时间后安排在消息队列中添加的任务.
Vla*_*nea 19
这里有相互矛盾的赞成答案,没有证据就没有办法知道相信谁.这是证明@DVK是正确的并且@SalvadorDali是错误的.后者声称:
"这里是为什么:.这是不可能拥有的setTimeout以0毫秒的延迟时间的最小值是由浏览器确定,它不是0毫秒历史浏览器设置这个最低至10毫秒,但HTML5规范和现代浏览器设置为4毫秒."
4ms的最小超时与发生的事情无关.真正发生的是setTimeout将回调函数推送到执行队列的末尾.如果在setTimeout(回调,0)之后你有阻塞代码需要几秒钟才能运行,那么回调将不会执行几秒钟,直到阻塞代码完成.试试这段代码:
function testSettimeout0 () {
var startTime = new Date().getTime()
console.log('setting timeout 0 callback at ' +sinceStart())
setTimeout(function(){
console.log('in timeout callback at ' +sinceStart())
}, 0)
console.log('starting blocking loop at ' +sinceStart())
while (sinceStart() < 3000) {
continue
}
console.log('blocking loop ended at ' +sinceStart())
return // functions below
function sinceStart () {
return new Date().getTime() - startTime
} // sinceStart
} // testSettimeout0
Run Code Online (Sandbox Code Playgroud)
输出是:
setting timeout 0 callback at 0
starting blocking loop at 5
blocking loop ended at 3000
in timeout callback at 3033
Run Code Online (Sandbox Code Playgroud)
Poi*_*nty 14
这样做的一个原因是将代码的执行推迟到单独的后续事件循环.当响应某种浏览器事件(例如,鼠标单击)时,有时只有在处理当前事件后才需要执行操作.该setTimeout()
设施是最简单的方法.
现在编辑它是2015年我应该注意到,也有requestAnimationFrame()
,这不完全相同,但它足够接近setTimeout(fn, 0)
它值得一提.
这是旧答案的旧问题.我想在这个问题上添加一个新的外观,并回答为什么会发生这种情况而不是为什么这有用.
所以你有两个功能:
var f1 = function () {
setTimeout(function(){
console.log("f1", "First function call...");
}, 0);
};
var f2 = function () {
console.log("f2", "Second call...");
};
Run Code Online (Sandbox Code Playgroud)
然后按以下顺序调用它们,f1(); f2();
只是为了看到第二个先执行.
这就是为什么:不可能有setTimeout
0毫秒的时间延迟.的最小值是由浏览器确定并且不为0毫秒.历史上,浏览器将此最小值设置为10毫秒,但HTML5规范和现代浏览器将其设置为4毫秒.
如果嵌套级别大于5,并且超时小于4,则将超时增加到4.
也来自mozilla:
要在现代浏览器中实现0 ms超时,可以使用此处所述的window.postMessage().
阅读以下文章后获取PS信息.
这两个评价最高的答案都是错误的。查看并发模型和事件循环上的MDN描述,应该清楚发生了什么(该MDN资源是真正的宝石)。而且只需使用 setTimeout
可添加在除了你的代码意想不到的问题,以“解决”这个小问题。
什么是真正回事是不是“浏览器可能没有完全准备好,因为并发性”,或基于什么“每一行是被添加到队列的后面的事件”。
DVK提供的jsfiddle确实说明了一个问题,但是他对此的解释不正确。
他的代码中发生的事情是他首先将事件处理程序附加到按钮click
上的事件#do
。
然后,当您实际单击该按钮时,将message
引用事件处理函数创建一个,该函数添加到中message queue
。当event loop
到达此消息时,它将frame
在堆栈上创建一个,并在jsfiddle中调用click事件处理函数。
这就是它变得有趣的地方。我们习惯于将Javascript视为异步的,因此我们容易忽略了这个小小的事实:在执行下一帧之前,必须完整执行任何框架。没有并发的人。
这是什么意思?这意味着无论何时从消息队列中调用一个函数,它都会阻塞该队列,直到其生成的堆栈被清空为止。或者,更笼统地说,它阻塞直到函数返回。并且它阻止了所有内容,包括DOM渲染操作,滚动和其他功能。如果要确认,只需尝试增加小提琴中长时间运行的操作的持续时间(例如,再运行10次外循环),您会注意到,在运行时,您无法滚动页面。如果运行时间足够长,您的浏览器会询问您是否要终止该进程,因为这会使页面无响应。该框架正在执行,并且事件循环和消息队列一直停留到完成。
那么,为什么文本的这种副作用没有更新?因为尽管你已经改变了DOM元素的值-你可以console.log()
它的值后立即改变它,看到它已经被改变(这说明了为什么DVK的解释是不正确的) -浏览器正在等待堆耗尽(on
返回的处理函数),并因此完成消息,以便最终可以执行由运行时添加的消息,作为对我们的变异操作的反应,并在UI中反映该变异。
这是因为我们实际上正在等待代码完成运行。我们没有说过“有人先获取它,然后用结果调用此函数,谢谢,现在我完成了imma return,现在就做任何事情”,就像我们通常使用基于事件的异步Javascript一样。我们输入一个click事件处理函数,我们更新一个DOM元素,调用另一个函数,另一个函数工作了很长时间然后返回,然后我们更新了相同的DOM元素,然后从初始函数返回,有效地清空了堆栈。而随后该浏览器可以得到队列中的下一条消息,这可能是由“上-DOM突变”型事件触发一些内部很可能是由美国产生的消息。
在当前执行的框架完成(函数返回)之前,浏览器UI无法(或选择不更新)UI。就我个人而言,我认为这是设计使然而不是限制。
那setTimeout
事情为什么起作用呢?这样做是因为它有效地从其自身的框架中删除了对长时间运行的函数的调用,将其调度为在window
上下文中稍后执行,因此它本身可以立即返回并允许消息队列处理其他消息。想法是,在更改DOM中的文本时,由我们在Javascript中触发的UI“更新”消息现在位于为长时间运行的功能排队的消息之前,因此UI更新发生在我们阻止之前需很长时间。
请注意,a)长时间运行的函数在运行时仍会阻塞所有内容,并且b)您不能保证UI更新实际上在消息队列中排在它之前。在我2018年6月的Chrome浏览器上,值0
不能“解决”小提琴演示的问题— 10可以解决。实际上,我对此感到有点窒息,因为在我看来,应该将UI更新消息先排队,因为它的触发器是在将长时间运行的函数调度为“稍后”运行之前执行的。但是也许V8引擎中有一些优化可能会干扰,或者也许我缺乏理解。
好的,那么使用问题setTimeout
是什么,对于这种特殊情况有什么更好的解决方案?
首先,setTimeout
在这样的事件处理程序上使用以尝试缓解另一个问题的问题很容易与其他代码混淆。这是我工作中的一个真实示例:
一位对事件循环有误解的同事试图通过使用一些模板渲染代码setTimeout 0
来渲染Java 脚本。他不再在这里问,但是我可以假设他插入了计时器以评估渲染速度(这将是函数的返回即时性),并发现使用这种方法可以使该函数快速响应。
第一个问题是显而易见的。您无法线程化javascript,因此在添加混淆时,您在这里一无所获。其次,您现在已经有效地从可能的事件侦听器堆栈中分离了模板的呈现,这些事件侦听器可能希望已经呈现了非常好的模板,而实际上可能还没有。现在,该功能的实际行为是不确定的,就像在不知不觉中一样,任何会运行该功能或依赖该功能的功能都是不确定的。您可以进行有根据的猜测,但不能对其行为进行适当的编码。
编写依赖于其逻辑的新事件处理程序时的“修复” 也要使用setTimeout 0
。但是,这不是一个解决办法,很难理解,调试由此类代码引起的错误也没有意思。有时永远不会有问题,有时它总是会失败,然后又一次,有时它会工作并偶尔中断,这取决于平台的当前性能以及当时发生的任何其他情况。这就是为什么我本人会建议不要使用此hack(这是 hack,我们都应该知道),除非您真的知道自己在做什么以及后果如何。
但是,什么可以我们做的呢?嗯,正如所引用的MDN文章所建议的那样,或者将工作拆分为多个消息(如果可以),以便排队的其他消息可以与您的工作交错并在其运行时执行,或者使用可以运行的Web worker与您的网页并列,并在完成计算后返回结果。
哦,如果您在想,“好吧,我不能只在长时间运行的函数中放一个回调以使其异步吗?”,然后否。回调并不会使其异步,它仍必须在明确调用回调之前运行长时间运行的代码。
归档时间: |
|
查看次数: |
231353 次 |
最近记录: |