setTimeout的小尴尬
我们都知道,alert这种内置弹框会阻塞后续代码执行:
之所以如此,就是因为JavaScript代码在浏览器中是单线程执行的。换句话说,浏览器中只有一个主线程负责运行所有JavaScript代码(不考虑Web Worker)。
提到浏览器中的JavaScript,基本上只有三个来源:
BOM API的代码,让我们可以操作并利用浏览器提供的能力
DOM API的代码,让我们可以操作网页内容
我们自己写的ECMAScript代码
这没什么。我们也知道,setTimeout用于“定时”执行代码,比如这样可以定时在3秒钟之后执行一段代码(函数):
当然,我们也都知道,setTimeout的“定时”并不精确,它只能保证delayCode函数在3秒以后执行,至于在3秒以后还要等多长时间才能执行,就跟它没关系了。
那跟什么有关系?我们知道,任务队列/事件循环是JavaScript执行异步任务的机制。setTimeout作为BOM API,它只负责设定一个计时器,到点把要执行的函数添加到任务队列。这样它就完成任务了。而把函数添加到任务队列并不能保证立即执行。什么时候能执行取决于事件循环什么时候把这个函数调度到主线程。事件循环调度异步函数的前提是主线程空闲。如果主线程被阻塞了,即使把函数添加到事件队列,事件循环也不会立即调度它到主线程。这就是setTimeout不能精确定时执行某个函数的原因。
显然,如果你的代码中存在依赖setTimeout精确定时的逻辑,就有可能遭遇尴尬。为此我们自己写代码时,除非绝对有把握,一定尽量不要依赖setTimout的精确定时。可是,问题在于我们能保证自己写的代码不依赖它,却很难保证我们代码依赖的第三方代码不依赖它。
小案例
下面我们就来介绍一个遭遇这种尴尬的真实案例。这个案例涉及的功能很简单,就是jQuery的$.ajax()函数在加载数据失败时重发请求。由于其超时逻辑依赖setTimeout的精确定时,结果导致超时设置失效。
相关代码也很简单,主要涉及3个函数:
function asyncRequest() {
$.ajax({
url: 'https://api.example.rs',
timeout: 15
}).then(success, fail)
} function success(data) {
// 正常处理数据
} function fail(xhr, errtext, errthrown) {
// 重发请求
asyncRequest()
// 弹框提示;阻塞主进程
alert('请求超时')
}
// 首次调用
asyncRequest()
asyncRequest:包含Ajax请求的函数,会在fail中再次调用
success:Ajax请求成功的回调
fail:Ajax请求失败的回调
正常逻辑是这样的:调用asyncRequest发送请求,成功则浏览器将success添加到任务队列,失败则浏览器将fail添加到任务队列。之后由事件循环将它们调度到主线程执行。success就是正常处理数据,而fail会先调用asyncRequest重发请求,再调用alert弹框提示。
测试环境下Ajax请求100毫秒左右可以返回。而为了测试超时失败后的逻辑,我们故意将超时时间设置为15毫秒,确保一定会超时。实际测试时,首次请求超时,走fail分支,重发请求、弹框,都没问题。但是,在鼠标点击关闭弹框后,却发现重发的请求正常返回了,并没有因超时被取消掉。反复测试都是如此。
这就尴尬了,到底为什么呢?研究发现,jQuery干掉超时请求的代码是这样的(https://j11y.io/jquery/#v=git&fn=jQuery.ajax):
也就是说,在我们设置了timeout选项的情况下,jQuery会通过setTimeout设置一个15毫秒后定时执行的函数,用来中断(abort)请求,我们称其为中断函数。
正常情况下,执行完上面的代码,浏览器会在15毫秒后把中断函数添加到任务队列上。此时如果主线程是空闲的,则事件循环会立即把这个函数调度到主线程去执行,请求被取消,浏览器把fail添加到任务队列,事件循环把它调度到主线程执行。这正是首次调用asyncRequet的情况。
第二次调用asyncRequest时有什么不同呢?不同之处在于这次调用完asyncRequest之后,还弹框阻塞了主线程。调用asyncRequest的结果跟之前一样,浏览器仍然会在15毫秒后把中断函数添加到任务队列。但是,这里要注意,由于此时主线程因弹框阻塞一直处于被占用状态,事件循环只能等待。直到我们手拿鼠标花一两秒时间把弹框关闭,主线程空闲出来,中断函数才会被调度到主线程上执行。而在此之前,Ajax请求早已成功返回,同时浏览器把success添加到任务队列。
理论上,Ajax请求返回后jqXHR(XMLHttpRequest)对象的状态不应再有任何改变(改变也没意义)。因此,中断函数的执行并不会改变“请求已经成功返回”这个事实。更为尴尬的是——中断函数执行后,紧接着,事件循环又把success函数调度到主线程。而fail函数根本就没有进入任务队列,更谈不上执行了。
小收获
通过上面的案例分析,我们看到本该“超时”失败的请求,因为中断函数被耽误在任务队列上迟迟得不到执行,最终反而成功返回了数据。当然,问题的根源在于alert弹框阻塞了主线程,以及JavaScript的异步机制(事件循环)。
至于jQuery依赖setTimeout取消超时请求的逻辑,只要不是遇到像本文案例这样长时间阻塞主进程的情况就不会有问题。在本案例中,如果不是为了测试而把超时时间设置得那么短,而是设置为比如5000毫秒,这个尴尬的局面也不会出现。假如实际的服务器响应时间真超过了5秒,只要我们在Ajax请求返回前关掉弹框,中断函数还是会先一步执行,从而取消未完成的请求。当然,实践中使用系统弹框阻塞主进程本来也不是推荐的做法。
不管怎么样,机缘巧合,我们还是借这个小尴尬(重温或者)深入理解了setTimeout乃至JavaScript(应该说浏览器提供的JavaScript运行时)的异步代码执行机制。那么在今后的编程实践中,我们就可以有意识地在逻辑中避免依赖setTimeout精确定时,因为它的定时真的不可靠啊!
setTimeout的小尴尬的更多相关文章
- gets() 与 scanf() 的小尴尬
gets() 与 scanf() 函数相处呢有点小尴尬的,就是 gets() 在 scanf() 后边就爱捣乱.为什么呢,先了解它们两者之间的异同: 同: 都是可以接受连续的字符数据 并在字符结束后自 ...
- 解决npm install卡住不动的小尴尬
npm install卡顿问题记录 遇到的问题 npm install -g @angular/cli 安装angular cli工具时,发现进度条一直卡住不动,相信很多朋友也遇到过.原因应该是国内的 ...
- setTimeout设置为0的作用
调用方式:iTimerID = window.setTimeout(vCode, iMilliSeconds [, sLanguage])功能:Evaluates an expression afte ...
- 【原】HTML5 新增的结构元素——能用并不代表对了
做移动端有一段时间,今天有同事问了我 article 和 section 标签的使用,模模糊糊的解释了下,他似懂非懂,有点小尴尬.忽然间觉得自己有必要再翻翻书籍,重温下 html5 的新元素.html ...
- HDU 5045 Contest
pid=5045">主题链接~~> 做题感悟:比赛时这题后来才写的,有点小尴尬.两个人商议着写写了非常久才写出来,I want to Powerful ,I believe me ...
- MY WAY程序(十八) 团队开发
1.通信知识 要了解的强哥的移动通信先验知识布局.我和另外一个毕业生有自己的学习,但我真的没有认真看.了解了一下,其余大部分时间在搞nodejs.另外一个应届毕业生则是按着一本电子书 ...
- ASP.NET Core MVC压缩样式、脚本及总是复制文件到输出目录
前言 在.NET Core之前对于压缩样式文件和脚本我们可能需要借助第三方工具来进行压缩,但在ASP.NET MVC Core中则无需借助第三方工具来完成,本节我们来看看ASP.NET Core MV ...
- 关于使用git和github的一点点感想
第二篇博客 首先附上我的第一个java程序github地址: https://github.com/KingsC123456/FirstJavaHello 其次是关于我的github介绍,因为一直使用 ...
- 【转】Nginx反向代理和负载均衡
原文链接:http://www.cnblogs.com/shuoer/p/7820899.html Nginx反向代理和负载均衡 环境说明 由于我使用的是windows系统,所以我用虚拟机虚拟出来了3 ...
随机推荐
- Nmon监控性能分析
一.CPU信息 1.折线图中蓝线为cpu占有率变化情况:粉线为磁盘IO的变化情况: 2.下面表各种左边的位磁盘的总体数据,包括如下几个: Avg tps during an interval 每个间隔 ...
- python __getattr__ & __getattribute__ 学习
实例属性的获取和拦截, 仅对实例属性(instance, variable)有效, 非类属性 getattr: 适用于未定义的属性, 即该属性在实例中以及对应的类的基类以及祖先类中都不存在 1. 动态 ...
- 攻防世界WEB高手进阶之blgdel
CISCN final 打开页面 扫描目录 Robots.txt Config.txt 代码审计 <?php class master { private $path; private $nam ...
- Centos7永久修改IP地址(NAT模式)
永久修改IP地址,即为设置静态的IP地址. 一.修改IP地址前需要准备的工作 1.虚拟机需要使用NAT的网络模式 虚拟机关机状态下,点击"编辑虚拟机设置",点击"网络适配 ...
- 详解Eureka 缓存机制
原文:https://www.cnblogs.com/yixinjishu/p/10871243.html 引言 Eureka是Netflix开源的.用于实现服务注册和发现的服务.Spring Clo ...
- PAT 乙级 1020.月饼 C++/Java
题目来源 月饼是中国人在中秋佳节时吃的一种传统食品,不同地区有许多不同风味的月饼.现给定所有种类月饼的库存量.总售价.以及市场的最大需求量,请你计算可以获得的最大收益是多少. 注意:销售时允许取出一部 ...
- 如何将VOC XML文件转化成COCO数据格式
数据转换实在是个烦人的工作,被折磨了很久决定抽出时间整理一下,仅供参考. 在一个项目中,我需要将已有的VOC的xml标注文件转化成COCO的数据格式,为了方便理解,文章按如下顺序介绍: XML文件内容 ...
- php工厂方法模式(factory method pattern)
继续练 <?php /* The factory method pattern deals with the problem of creating objects without having ...
- Beta冲刺(3/7)——2019.5.24
所属课程 软件工程1916|W(福州大学) 作业要求 Beta冲刺(3/7)--2019.5.24 团队名称 待就业六人组 1.团队信息 团队名称:待就业六人组 团队描述:同舟共济扬帆起,乘风破浪万里 ...
- Vue 实例暴露了一些有用的实例属性与方法。这些属性与方法都有前缀 $,以便与代理的 data 属性区分
var data = { a: 1 } var vm = new Vue({ el: '#example', data: data }) vm.$data === data // -> true ...