setTimout( , 0) 详解
setTimout( , 0)
一、前言
前端工程师们工作久了,一般都会在某些地方看见过这样的代码:
setTimeout(function(){
// TODO
}, 0);
举个实例,移动端我们经常会用的一个库叫做iScroll来模仿iOS系统里面的滚动反弹效果,而它的官方文档里面就有类似的代码建议:
上面其实也说到了setTimeout( , 0)的作用,就是当你改动了DOM后,让浏览器有一点空余的时间来重绘这个页面。可能道理大家都懂,但是为什么啊??下面让我们通过实例来研究说明setTimeout(, 0)的工作原理。
二、stackoverflow解释翻译(原文地址)
stackoverflow是程序员的好基友,因此我在那里翻到了这个解释,并且对其进行了中文翻译,英语好的同学可以直接到上面原文地址里查看。整个解释非常的详细:
想象一下页面中有一个 ”do something“ 按钮和一个显示结果的 DIV。
”do something“ 按钮中点击事件
onclick
的回调函数 ”LongCalculate()“ 中干了两件事:
- 执行一个非常耗时的计算(大约3分钟)。
- 把上面计算的结果输出到结果 DIV 里面。
现在,你的用户开始测试这个功能,点击 “do something” 按钮,接着页面就似乎在3分钟内什么也没干,用户烦躁不安,再次点了一下按钮,又等了一分钟,也是什么都没有发生,然后再次点击了按钮。。。
问题很明显:你需要有一个“状态” DIV,用来展示现在进行的情况。下面展示的最新的处理。
所以你添加了一个“状态” DIV(刚开始是空的),接着调整
onclick
的回调函数(函数LongCalc()
),调整后该回调函数执行以下4个步骤:
- 改变状态 DIV 的内容为 “Calculating... may take ~3 minutes” 。
- 执行一个非常耗时的计算(大约3分钟)。
- 把上面计算的结果输出到结果 DIV 里面。
- 改变状态DIV的内容为 “Calculation done”。
修改完毕,你兴高采烈地叫你的用户再来测试以下。
他们满脸不爽的又走过来说,他们点击按钮的时候,状态 DIV 根本都不会显示 "Calculation..." 这个状态!!!
你绞尽脑汁,万思不得其解。到 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);
});
三、结合Timeline工具分析
上面的解释已经很清楚了,但还是有点抽象。为了更进一步的加深对这个原理的理解,我个人使用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时的事件记录:
- 触发点击事件。
- 执行点击事件的回调函数。注意,这一步已经包括耗时的计算代码了。
- 只一次Layout事件和Paint事件!,而触发这些事件的代码为:
document.getElementById(status_div).innerText = 'calclation done';
。
这份记录和stackoverflow中的解释基本吻合,但还记得上面说过这样一句吗:
眼尖的读者可能注意到在计算完结之后 “Calculating” 在微秒之间一闪而过。
实际情况是用户永远没可能看到 “Calculating” 这个状态,因为浏览器的优化功能,把两个重绘操作合并成一个了。
接下来看看使用了setTimeout的情况:
- 触发点击事件
- 执行点击事件的回调函数。
- 设置一个Timer。这里可以看出执行setTimeout的时候会设置一个Timer,setTimeout的回调函数,只有当这个Timer完成倒计时才会执行回调函数。
- 执行由代码
status_ok.innerText = 'calculating...';
引起的重绘操作。 - Timer计时器倒计时完毕,执行里面的计算代码。
- 计算完成后,执行由代码
document.getElementById(status_div).innerText = 'calclation done';
引起的重绘操作。
因此,我们就可以很确定的说,setTimeout( , 0)的作用其实就是在进行复杂计算前,腾出一点时间让浏览器可以完成重绘相关的Layout、Paint等操作。
四、setTimeout( ,0)能百分百解决问题吗?
不知道大家看到这里有没有这样一个疑问: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样式太丑了。。。)
(如果对这篇文章有疑问,大家可以在下面评论,我会尽快给出答复。)
setTimout( , 0) 详解的更多相关文章
- 百度大脑UNIT3.0详解之嵌入式对话理解技术
相信很多人都体验过手机没有网时的焦虑,没有网什么也做不了.而机器人也会遇到这样的时刻,没有网或者网络环境不好的情况下,无法识别用户在说什么,也无法回复用户.在AIoT(AI+物联网)飞速普及的现在,智 ...
- 百度大脑UNIT3.0详解之知识图谱与对话
如今,越来越多的企业想要在电商客服.法律顾问等领域做一套包含行业知识的智能对话系统,而行业或领域知识的积累.构建.抽取等工作对于企业来说是个不小的难题,百度大脑UNIT3.0推出「我的知识」版块专门为 ...
- 百度大脑UNIT3.0详解之数据生产工具DataKit
在智能对话项目搭建的过程中,高效筛选.处理对话日志并将其转化为新的训练数据,是对话系统效果持续提升的重要环节,也是当前开发者面临的难题之一.为此百度大脑UNIT推出学习反馈闭环机制,提供数据获取.辅助 ...
- Mongostat 3.0详解
可以参考之前写的这篇博客: Mongostat 2.6详解 mapped Changed in version 3.0.0. Only for MMAPv1 Storage Engine. The t ...
- CM自动化安装CDH5.14.0详解
CDH5.14.0版本说明 CDH最早版本只包含hadoop.hive.hbase等基础组件,CDH5.14.0版本目前已经封装了spark.impala.kudu(CDH 5.13.x开始)等众多组 ...
- Android数据存储之GreenDao 3.0 详解
前言: 今天一大早收到GreenDao 3.0 正式发布的消息,自从2014年接触GreenDao至今,项目中一直使用GreenDao框架处理数据库操作,本人使用数据库路线 Sqlite----> ...
- iOS中 蓝牙2.0详解/ios蓝牙设备详解 韩俊强的博客
每日更新关注:http://weibo.com/hanjunqiang 新浪微博 整体布局如下: 程序结构如右图: 每日更新关注:http://weibo.com/hanjunqiang ...
- OAuth 2.0详解
OAuth是一个关于授权(authorization)的开放网络标准,在全世界得到广泛应用,目前的版本是2.0版. 本文对OAuth 2.0的设计思路和运行流程,做一个简明通俗的解释,主要参考材料为R ...
- Spring Boot admin 2.0 详解
一.什么是Spring Boot Admin ? Spring Boot Admin是一个开源社区项目,用于管理和监控SpringBoot应用程序. 应用程序作为Spring Boot Admin C ...
随机推荐
- android如何查看网卡名和ip
我们知道,在windows下查看ip地址用ipconfig,在Linux下查看ip地址用ifconfig.今天在使用android查看的时候ifconfig却不管用: 查找网上资料发现,原来默认ifc ...
- BZOJ_5338_ [TJOI2018]xor_可持久化trie
BZOJ_5338_ [TJOI2018]xor_可持久化trie Description 有一棵点数为N的树,树边有边权.给你一个在0~N之内的正整数K,你要在这棵树中选择K个点,将其染成黑色,并 ...
- 「UVA11181」 Probability|Given(概率
题意翻译 有n个人要去买东西,他们去买东西的概率为p[i]. 现在得知有r个人买了东西,在这种条件下,求每个人买东西的概率. 感谢@s_r_f 提供翻译 题目描述 PDF 输入输出格式 输入格式: 输 ...
- BZOJ1500:[NOI2005]维修数列
浅谈\(splay\):https://www.cnblogs.com/AKMer/p/9979592.html 浅谈\(fhq\)_\(treap\):https://www.cnblogs.com ...
- zynq基础
zynq交叉编译环境设置 OpenCV在Zedboard上的移植 ubuntu 下串口调试工具 minicom安装与配置
- jmeter性能指标
Aggregate Report 是 JMeter 常用的一个 Listener,中文被翻译为“聚合报告”.今天再次有同行问到这个报告中的各项数据表示什么意思,顺便在这里公布一下,以备大家查阅. 如果 ...
- Lagom学习(一)
Lagom是JAVA系下响应式 微服务框架,其特性包括: 目前,大多数已有的微服务框架关注于简化单个微服务的构建,Lagom将其扩展到了微服务所构成的系统,分布式系统的复杂性. 同步通信使用HTTP, ...
- Unity查找Editor下Project视图中特定的资源
[MenuItem("Tools/Check Text Count")] public static void CheckText () { //查找指定路径下指定类型的所有资源, ...
- petrozavodsk1
A 转化模型和相当于求解小于n/2的最大的和n互质的数字, 显然可以证明所求和n/2相距 O(logn) ,从 n/2 开始向下枚举然后判定即可. B 上下界网络流? C 从底层开始向上走贪心选下层节 ...
- sublime取消自动升级提示
1.进入Preferences -> Settings-User ,添加 "update_check": false, 2.重启Sublime.