setTimout( , 0)

一、前言

前端工程师们工作久了,一般都会在某些地方看见过这样的代码:

setTimeout(function(){
// TODO
}, 0);

举个实例,移动端我们经常会用的一个库叫做iScroll来模仿iOS系统里面的滚动反弹效果,而它的官方文档里面就有类似的代码建议:

上面其实也说到了setTimeout( , 0)的作用,就是当你改动了DOM后,让浏览器有一点空余的时间来重绘这个页面。可能道理大家都懂,但是为什么啊??下面让我们通过实例来研究说明setTimeout(, 0)的工作原理。

二、stackoverflow解释翻译(原文地址)

stackoverflow是程序员的好基友,因此我在那里翻到了这个解释,并且对其进行了中文翻译,英语好的同学可以直接到上面原文地址里查看。整个解释非常的详细:

想象一下页面中有一个 ”do something“ 按钮和一个显示结果的 DIV。

”do something“ 按钮中点击事件onclick的回调函数 ”LongCalculate()“ 中干了两件事:

  1. 执行一个非常耗时的计算(大约3分钟)。
  2. 把上面计算的结果输出到结果 DIV 里面。

现在,你的用户开始测试这个功能,点击 “do something” 按钮,接着页面就似乎在3分钟内什么也没干,用户烦躁不安,再次点了一下按钮,又等了一分钟,也是什么都没有发生,然后再次点击了按钮。。。

问题很明显:你需要有一个“状态” DIV,用来展示现在进行的情况。下面展示的最新的处理。


所以你添加了一个“状态” DIV(刚开始是空的),接着调整onclick的回调函数(函数LongCalc()),调整后该回调函数执行以下4个步骤:

  1. 改变状态 DIV 的内容为 “Calculating... may take ~3 minutes” 。
  2. 执行一个非常耗时的计算(大约3分钟)。
  3. 把上面计算的结果输出到结果 DIV 里面。
  4. 改变状态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 within onClick):

  1. 改变状态 DIV 的内容为 “Calculating... may take ~3 minutes” 。

  2. 执行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时的事件记录:

  1. 触发点击事件。
  2. 执行点击事件的回调函数。注意,这一步已经包括耗时的计算代码了。
  3. 只一次Layout事件和Paint事件!,而触发这些事件的代码为:document.getElementById(status_div).innerText = 'calclation done';

这份记录和stackoverflow中的解释基本吻合,但还记得上面说过这样一句吗:

眼尖的读者可能注意到在计算完结之后 “Calculating” 在微秒之间一闪而过。

实际情况是用户永远没可能看到 “Calculating” 这个状态,因为浏览器的优化功能,把两个重绘操作合并成一个了。


接下来看看使用了setTimeout的情况:

  1. 触发点击事件
  2. 执行点击事件的回调函数。
  3. 设置一个Timer。这里可以看出执行setTimeout的时候会设置一个Timer,setTimeout的回调函数,只有当这个Timer完成倒计时才会执行回调函数。
  4. 执行由代码status_ok.innerText = 'calculating...';引起的重绘操作。
  5. Timer计时器倒计时完毕,执行里面的计算代码。
  6. 计算完成后,执行由代码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) 详解的更多相关文章

  1. 百度大脑UNIT3.0详解之嵌入式对话理解技术

    相信很多人都体验过手机没有网时的焦虑,没有网什么也做不了.而机器人也会遇到这样的时刻,没有网或者网络环境不好的情况下,无法识别用户在说什么,也无法回复用户.在AIoT(AI+物联网)飞速普及的现在,智 ...

  2. 百度大脑UNIT3.0详解之知识图谱与对话

    如今,越来越多的企业想要在电商客服.法律顾问等领域做一套包含行业知识的智能对话系统,而行业或领域知识的积累.构建.抽取等工作对于企业来说是个不小的难题,百度大脑UNIT3.0推出「我的知识」版块专门为 ...

  3. 百度大脑UNIT3.0详解之数据生产工具DataKit

    在智能对话项目搭建的过程中,高效筛选.处理对话日志并将其转化为新的训练数据,是对话系统效果持续提升的重要环节,也是当前开发者面临的难题之一.为此百度大脑UNIT推出学习反馈闭环机制,提供数据获取.辅助 ...

  4. Mongostat 3.0详解

    可以参考之前写的这篇博客: Mongostat 2.6详解 mapped Changed in version 3.0.0. Only for MMAPv1 Storage Engine. The t ...

  5. CM自动化安装CDH5.14.0详解

    CDH5.14.0版本说明 CDH最早版本只包含hadoop.hive.hbase等基础组件,CDH5.14.0版本目前已经封装了spark.impala.kudu(CDH 5.13.x开始)等众多组 ...

  6. Android数据存储之GreenDao 3.0 详解

    前言: 今天一大早收到GreenDao 3.0 正式发布的消息,自从2014年接触GreenDao至今,项目中一直使用GreenDao框架处理数据库操作,本人使用数据库路线 Sqlite----> ...

  7. iOS中 蓝牙2.0详解/ios蓝牙设备详解 韩俊强的博客

    每日更新关注:http://weibo.com/hanjunqiang  新浪微博 整体布局如下:     程序结构如右图: 每日更新关注:http://weibo.com/hanjunqiang  ...

  8. OAuth 2.0详解

    OAuth是一个关于授权(authorization)的开放网络标准,在全世界得到广泛应用,目前的版本是2.0版. 本文对OAuth 2.0的设计思路和运行流程,做一个简明通俗的解释,主要参考材料为R ...

  9. Spring Boot admin 2.0 详解

    一.什么是Spring Boot Admin ? Spring Boot Admin是一个开源社区项目,用于管理和监控SpringBoot应用程序. 应用程序作为Spring Boot Admin C ...

随机推荐

  1. C语言的内存四区模型和函数调用模型

    首先是操作系统将代码程序加载到内存中 然后将内存分为4个区 栈区,程序的局部变量区,函数传递的参数,由编译器自动进行内存资源的释放. 堆区,动态内存申请,如果不手动释放内存,则这块内存不会进行析构. ...

  2. HihoCoder1650 : 扁平化管理([Offer收割]编程练习赛38)(二分)

    描述 小Hi的公司包括CEO在内一共有N名员工.这N名员工的上下级关系形成树形结构,CEO处于树根,普通员工处于叶子节点. 现在公司希望管理扁平化,要求树形结构中的层级不超过L层.此外,假设A是B的直 ...

  3. Win 10 无法打开内核设备“\\.\Global\vmx86”

    Win 10操作系统, VMWareWorkstation10 无法打开内核设备“\\.\Global\vmx86”: 系统找不到指定的文件.你想要在安装 VMware Workstation 前重启 ...

  4. poj1733 Parity game[带权并查集or扩展域]

    地址 连通性判定问题.(具体参考lyd并查集专题该题的转化方法,反正我菜我没想出来).转化后就是一个经典的并查集问题了. 带权:要求两点奇偶性不同,即连边权为1,否则为0,压缩路径时不断异或,可以通过 ...

  5. dataguard 常规运维操作

    1.standby database 正常的打开与关闭 1.1 针对 physical standby打开:--打开数据库SQL>startup; -- 开启 mgr 进程 SQL>ALT ...

  6. SoapUI登录测试(2)-- 断言

    SoapUI登录测试(1)的结果为: 可以看到只有第2步是成功的,1.3的结果是unKnown,这里并没有对1.3两步添加断言,判断testCase中的这2步是否通过. 一.添加断言 1. /logi ...

  7. Autofac依赖注入框架使用

    简介: Autofac是一款IOC框架,比较于其他的IOC框架,如Spring.NET,Unity,Castle等等所包含的,它很轻量级性能上非常高 控制反转和依赖注入: 控制反转 IOC(Inver ...

  8. Elasticsearch之match_phrase小坑记录

    1.问题抛出 某个词组在Elasitcsearch中的某个document中存在,就一定通过某种匹配方式把它搜出来. 举例: title=公路局正在治理解放大道路面积水问题. 输入关键词:道路,能否搜 ...

  9. <c和指针>学习笔记1之快速上手和基本概念

    1 c语言中的注释 功能:使这段代码在程序中不起作用,当然如果是功能注释,那是方便其他人阅读您的代码. 大部分情况下,多行的注释,我们采用的是这种方式,例如  /*内容*/. 这个符号不能嵌套,也就是 ...

  10. 3、opencv 图像显示功能

    #include <iostream>#include <opencv2/opencv.hpp>using namespace cv;using namespace std;i ...