前端工程师们工做久了,通常都会在某些地方看见过这样的代码:javascript
setTimeout(function(){ // TODO }, 0);
举个实例,移动端咱们常常会用的一个库叫作iScroll来模仿iOS系统里面的滚动反弹效果,而它的官方文档里面就有相似的代码建议:html
上面其实也说到了setTimeout( , 0)的做用,就是当你改动了DOM后,让浏览器有一点空余的时间来重绘这个页面。可能道理你们都懂,可是为何啊??下面让咱们经过实例来研究说明setTimeout(, 0)的工做原理。前端
想象一下页面中有一个 ”do something“ 按钮和一个显示结果的 DIV。java
”do something“ 按钮中点击事件
onclick
的回调函数 ”LongCalculate()“ 中干了两件事:git
- 执行一个很是耗时的计算(大约3分钟)。
- 把上面计算的结果输出到结果 DIV 里面。
如今,你的用户开始测试这个功能,点击 “do something” 按钮,接着页面就彷佛在3分钟内什么也没干,用户烦躁不安,再次点了一下按钮,又等了一分钟,也是什么都没有发生,而后再次点击了按钮。。。程序员
问题很明显:你须要有一个“状态” DIV,用来展现如今进行的状况。下面展现的最新的处理。github
因此你添加了一个“状态” DIV(刚开始是空的),接着调整
onclick
的回调函数(函数LongCalc()
),调整后该回调函数执行如下4个步骤:api
- 改变状态 DIV 的内容为 “Calculating... may take ~3 minutes” 。
- 执行一个很是耗时的计算(大约3分钟)。
- 把上面计算的结果输出到结果 DIV 里面。
- 改变状态DIV的内容为 “Calculation done”。
修改完毕,你兴高采烈地叫你的用户再来测试如下。浏览器
他们满脸不爽的又走过来讲,他们点击按钮的时候,状态 DIV 根本都不会显示 "Calculation..." 这个状态!!!markdown
你绞尽脑汁,万思不得其解。到 StackOverflow疯狂提问(或者阅读文档和问Google),接着你发现问题所在了:
浏览器把全部事件触发的待执行任务( UI 任务和 JavaScript 命令)都放到同一个队列里面。而且不幸的是,重绘状态 DIV 的内容为 ”Calculating...“ 是一个分离的待执行任务,这个任务会放到队列的最后面!
下面是你的用户测试过程当中的事件分解和队列中的内容:
- 队列:
[Empty]
- 事件:点击按钮。事件触发后队列的内容:
[Execute OnClick handler(line 1-4)]
- 事件:执行回调函数的第一行代码(也就是改变状态 DIV 的值)。事件触发后队列的内容:
[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]
。- 事件:执行回调函数的第三行代码(计算结果输出到结果 DIV )。事件触发后队列的内容:
[Execute OnClick handler(line 4), re-draw Status DIV with "Calculating" value, re-draw result DIV with result]
。- 事件:执行回调函数的第四行代码(结果 DIV 的状态改成 “DONE” )。事件触发后队列的内容:
[Execute OnClick handler, re-draw Status DIV with "Calculating" value, re-draw result DIV with result; re-draw Status DIV with "DONE" value]
。- 事件:执行回调函数隐含的
return
。从队列中移除 “Execute OnClick handler”,而后执行队列中的下一个任务。- 注意:因为咱们已经完成了计算,3分钟已通过去。重绘事件尚未发生!!!
- 事件:重绘状态 DIV 的内容为 “Calculating” 。把这个重绘任务从队列中去掉。
- 事件:使用计算的结果重绘结果 DIV 。把这个重绘任务从队列中去掉。
- 事件:重绘状态 DIV 为 “Done”。把这个重绘任务从队列中去掉。眼尖的读者可能注意到在计算完结以后 “Calculating” 在微秒之间一闪而过。
所以,潜在的问题就是重绘状态 DIV 这个事件被放到了队列的最后,放到了耗时3分钟的计算后面,因此这个重绘在计算完成前都没有执行。
要解决这个问题,就要使用
setTimeout()
了。那么怎样解决?由于经过setTimeout
调用须要长时间执行的代码的时候,实际上是建立了两个事件:setTimeout
自身的执行事件,和以后才进队列的代码执行事件。(因为 0 秒 timeout)So, to fix your problem, you modify your
onClick
handler to be TWO statements (in a new function or just a block withinonClick
):
改变状态 DIV 的内容为 “Calculating... may take ~3 minutes” 。
执行
setTimeout()
,在0秒后执行LongCalc()
函数。
LongCalc()
函数基本上和上面的同样,但明显地,不用再在里面改变状态 DIV 的内容为 “Calculating” ,并且计算也不会马上执行。因此呢,如今的事件顺序和队列会变成怎样呢?
- 队列:
[Empty]
- 事件:点击按钮。事件触发后队列的内容:
[Execute OnClick handler(status update, setTimeout() call)]
- 事件:执行onclick回调函数中的第一行(改变状态 DIV 的值)。事件触发后队列的内容:
[Execute OnClick handler(which is a setTimeout call), re-draw Status DIV with new "Calculating" value]
。- 事件:执行onclick回调函数中的第二行(执行 setTimeout )。事件触发后队列的内容:
[re-draw Status DIV with "Calculating" value]
。队列在0+秒内不会有新事件入栈。- 事件:0+秒以后timeout计时器完成计时。事件触发后队列的内容:
[re-draw Status DIV with "Calculating" value, execute LongCalc (lines 1-3)]
。- 事件:重绘状态 DIV 的内容为 ”Calculating“。事件触发后队列的内容:
[execute LongCalc (lines 1-3)]
。注意,此次的重绘事件可能会在timeout计时器完成计时以前执行,不过这不要紧。- ...
万岁 ! 状态 DIV 在执行计算前成功更新为 “Calculating...” !!!下面是JSFiddle中解释这个例子的代码:http://jsfiddle.net/C2YBE/31/
HTML code:
<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>JavaScript code: (Executed on onDomReady and may require 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('calclation 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); });
上面的解释已经很清楚了,但仍是有点抽象。为了更进一步的加深对这个原理的理解,我我的使用Chrome的Timeline工具再进行一次分析,也看看有没有什么新的发现。
为了使数据更加清晰,我把上面js中的jQuery代码都更换为原生的api。流程内容其实什么都没有改变:
var status_ok = document.getElementById('status_ok'); var do_ = document.getElementById('do'); var status = document.getElementById('status'); var do_ok = document.getElementById('do_ok'); 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; } } } document.getElementById(status_div).innerText = 'calclation done'; } // Assign events to buttons do_.onclick = function() { status.innerText = 'calculating....'; long_running('status'); }; do_ok.onclick = function() { status_ok.innerText = 'calculating...'; window.setTimeout(function() {long_running('status_ok')}, 0); };
接下来再放上Timeline的两张事件记录图,左边为没有使用setTimeout的,右边为使用了setTimeout的:
先看看没有使用setTimeout时的事件记录:
document.getElementById(status_div).innerText = 'calclation done';
。这份记录和stackoverflow中的解释基本吻合,但还记得上面说过这样一句吗:
眼尖的读者可能注意到在计算完结以后 “Calculating” 在微秒之间一闪而过。
实际状况是用户永远没可能看到 “Calculating” 这个状态,由于浏览器的优化功能,把两个重绘操做合并成一个了。
接下来看看使用了setTimeout的状况:
status_ok.innerText = 'calculating...';
引发的重绘操做。document.getElementById(status_div).innerText = 'calclation done';
引发的重绘操做。所以,咱们就能够很肯定的说,setTimeout( , 0)的做用其实就是在进行复杂计算前,腾出一点时间让浏览器能够完成重绘相关的Layout、Paint等操做。
不知道你们看到这里有没有这样一个疑问:setTimeout( ,0)腾出的时间必定足够让浏览器执行Layout、Update Layer Tree和Paint等一连串的动做吗?先给出一个答案,不必定!
在这里我继续抛出一张图,这张图是我用上面如出一辙的代码记录出来的(使用setTimeout的状况下):
你们注意到红框里面的内容了吗,浏览器要绘制 “calculating...” 的最后一步Paint事件前,Timer计时器倒计时完毕,执行计算代码了!因此最终都没有Paint出来!执行完计算以后,直接合并重绘操做,显示内容 “calclation done” 了。因此此次即便是用了setTimeout( , 0)我也是看不到 “calculating...” 这个状态的。
因此为了保证每次的显示效果都正常,你们能够把setTimeout( , 0)中的倒计时间设置更久,例如20、30又或者200、300。具体应该是多少须要根据咱们重绘DOM的复杂程度来决定。
其实上面我给出的iScroll文档说明中也说明过这个问题:
Consider that if you have a very complex HTML structure you may give the browser some more rest and raise the timeout to 100 or 200 milliseconds.
This is generally true for all the tasks that have to be done on the DOM. Always give the renderer some rest.
最后的总结:使用setTimeout( , 0)可让咱们在进行复杂运算前腾出时间,使浏览器完成渲染页面相关的操做。进行复杂的渲染时,也要相对的把倒计时的时间延长,以保证有足够的时间。
(你们还能够到个人Github上面得到更好的阅读体验,由于博客园的markdown样式太丑了。。。)
(若是对这篇文章有疑问,你们能够在下面评论,我会尽快给出答复。)