本文仅是技术验证,记录,交流,不针对任何人。有冒犯的地方,请谅解。本文首发于https://vsnail.cn/static/doc/blog/setTimeout.htmlhtml
本想喝着coffee
,看着娃,过一个恬静的周六。occasionally
,浏览到一段代码,觉的蛮有趣。java
setTimeout(function(){console.log(1)},30)
setTimeout(function(){console.log(2)},10)
setTimeout(function(){console.log(3)},0)
let now = new Date();
while(new Date() - now<100){
}
console.log(0);
复制代码
估计大部分人都会知道第一个输出会是0
(若是还不知道为何0
会先输出,也没有关系,看完整篇文章你就会知道了). 可是后面输出的顺序究竟是3>2>1
仍是1>2>3
,估计就有争议了。web
回手掏,鬼刀一开看不见,走位走位,手里干chrome
想知道上面的答案?等着,让咱们先来看看setTimeout
相关基础。segmentfault
setTimeout()
方法能够设置一个定时器,该定时器在定时器到期后执行一个函数或指定的一段代码。浏览器
最简单的示例:缓存
100ms
后弹出系统对话框。(非严谨的,用俚语表述的需求。。。莫怪)数据结构
setTimeout(function(){
alert('走位,走位')
},100)
复制代码
好简单的,是不?地球人都知道的东西,再写就没意思了。接下来写点可能会不知道的东东。多线程
let timer = window.setTimeout(fun\[,delay,param1,param2,...]);
复制代码
咱们经常使用的就两个参数,估计一个参数,或者多于两个参数的状况用的比较少。只有一个参数时,延迟时间默认为0;有多于两个参数时,除开第一和第二参数的其余参数,咱们称之为“附加参数”。附加参数都会作为回调函数的参数传递。app
let func = function(a,b){
console.log(a+b);
}
setTimeout(func,100,10,20); //30
复制代码
防抖:在事件被触发n秒后再执行回调,若是在这n秒内又被触发,则从新计时。
function debounce(fn, wait) {
var timer = null;
return function () {
var context = this
var args = arguments
if (timer) {
clearTimeout(timer);
timer = null;
}
timer = setTimeout(function () {
fn.apply(context, args)
}, wait)
}
}
复制代码
js
中可使用setInterval
开启轮询,可是这种存在一个问题就是执行间隔每每就不是你但愿的间隔时间。使用setTimeout
构造轮询能保证每次轮询的间隔。
咱们都清楚js
是单线程的,意味着js处理大数据的时候,容易处于‘假死’状态。那么这个时候,咱们能够利用setTimeout
进行切片,来避免‘假死’状态的出现。
let func = function(index){
....
}
for(let i=0,l=10000000000;i<l;i++){
(function(index){
setTimeout(function(){func(index)},0)
})(i)
}
复制代码
基本setTimeout
经常使用的用法就是这些。
走位完了,让咱们一块儿回手掏掏他们的原理和机制。
咱们都知道,现代浏览器每一个标签页就是一个进程,每一个进程下面又包含了各类线程,好比javaScript
线程,渲染线程,请求线程等等。也就是说js
是单线程的。估计有人要问了为何js
是单线程呢,为何不是多线程呢?其实这和js
的用途有关系。做为浏览器脚本语言,JavaScript
的主要用途是与用户互动,以及操做DOM
。这决定了它只能是单线程,不然会带来很复杂的同步问题。好比,假定JavaScript
同时有两个线程,一个线程在某个DOM
节点上添加内容,另外一个线程删除了这个节点,这时浏览器应该以哪一个线程为准?因此,为了不复杂性,从一诞生,JavaScript
就是单线程,这已经成了这门语言的核心特征,未来也不会改变。为了利用多核CPU
的计算能力,HTML5
提出Web Worker
标准,容许JavaScript
脚本建立多个线程,可是子线程彻底受主线程控制,且不得操做DOM
。因此,这个新标准并无改变JavaScript
单线程的本质。
OK,js
是单线程,那么咱们能够得出setTimeout
绝对不是开启另外一个线程来实现异步的。那setTimeout
是如何达到异步效果的呢?
在js
中,全部任务都分为同步任务和异步任务两大类。同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;异步任务指的是,不进入主线程、而进入"任务队列"(task queue
)的任务,只有"任务队列"通知主线程,某个异步任务能够执行了,该任务才会进入主线程执行。
(1)全部同步任务都在主线程上执行,造成一个执行栈(
execution context stack
)。
(2)主线程以外,还存在一个"任务队列"(task queue
)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
(3)一旦"执行栈"中的全部同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,因而结束等待状态,进入执行栈,开始执行。
(4)主线程不断重复上面的第三步。
只要主线程空了,就会去读取"任务队列",这就是JavaScript
的运行机制。这个过程会不断重复。
"任务队列"是一个先进先出的数据结构,排在前面的事件,优先被主线程读取。主线程的读取过程基本上是自动的,只要执行栈一清空,"任务队列"上第一位的事件就自动进入主线程。
主线程从"任务队列"中读取事件,这个过程是循环不断的,因此整个的这种运行机制又称为Event Loop
(事件循环)。
这里必定要分清楚task queue
和Event loop
概念。以前,发现不少人老是分不清楚task queue
和 Event loop
概念。说setTimeout
原理时,往往有人说到是将回调函数放入到事件队列里面,而后。。。。。。;真觉的这样的说法不太好。
我的推测,每当调用setTimeout
方法,其实是向一个缓存对象写入一个键值对(以数字为键,以回调函数为值)。当到达指定延迟时间后,才将回调函数放入到task queue
中,等待进入执行栈。
在回手掏完以后,基本上setTimeout
以及js
运行机制应该大概明白了。经过以上的原理及机制,咱们来分析一下下面的几个例子(包含以前没有说道的setTimeout
的注意项):
setTimeout(function(){console.log(1)},30)
setTimeout(function(){console.log(2)},10)
setTimeout(function(){console.log(3)},0)
let now = new Date();
while(new Date() - now<100){
}
console.log(0);
复制代码
这段代码在执行栈中执行顺序为:
- 声明变量
now
;
2. 向setTimeout缓存对象中放入延迟30毫秒执行的回调函数(这个回调函数咱们标记为func3);
3. 向setTimeout缓存对象中放入延迟10毫秒执行的回调函数(这个回调函数咱们标记为func2);
4. 向setTimeout缓存对象中放入延迟0毫秒执行的回调函数(这个回调函数咱们标记为func1);
5. 向变量now赋值当前时间;
6. 一直循环100ms;6.1. 在0ms时,将回调函数func1放入task queue中。
6.2. 在10ms时,将回调函数func2放入task queue中。
6.3. 在30ms时,将回调函数func3放入task queue中。
- 向控制台打印0;
- 执行栈空闲,从
task queue
中提取第一个任务(func1)。执行完func1后,再从task queue 中提取一个任务(func2)。执行完func2后,再从task queue 中提取一个任务(func3).
以上就是整个代码的大概执行流程。所以,咱们获得的打印顺序为 0>3>2>1。这个里面主要是要理解,调用setTimeout方法并非直接将回调函数放入task queue
中,而是等到到达指定延时后,才将回调函数放入task queue
中。
也许你常常用几秒或者几十秒作延迟时间,估计你不多会想到setTimeout
能设置的最大的延迟时间是多少呢?或者若是超出setTimeout
的最大延迟时长,又会怎么样?
在一篇文章上看到过,setTimout
最大延迟时长是用32位有符号数存储的,所以他的最大值应该是Math.pow(2,31)-1=2147483647
,那么换算整天,大约就是24.8
天。若是设置的时长大于2147483647
,那么setTimeout
的延时时长将会自动设置为0
;
setTimeout(function(){console.log(1)},2147483648)
setTimeout(function(){console.log(2)},2147483647)
复制代码
你会看到,控制台会当即输出1
,而2
却没有输出,若是上面的结论是正确的,要想看到2
,估计要等24.8
天了。嘿嘿,反正我是不许备等的了。。。有想尝试的兄弟,能够试了之后告知下。
在MDN上看到这么一句话,“delay
取默认值0
,意味着“立刻”执行,或者尽快执行。”
也就是说将延时时长设置为0
,是在有条件的状况下尽快执行。但真的是0
毫秒就放入task queue
中吗?
咱们来看段代码:
setTimeout(function(){
console.log(2)
},2)
setTimeout(function(){
console.log(6)
},6)
setTimeout(function(){
console.log(1)
},1)
setTimeout(function(){
console.log(3)
},3)
setTimeout(function(){
console.log(0)
},0)
复制代码
这样的一段代码,可能会有些人认为他的输出结果是:0>1>2>3>6
.实际状况却不是这样的,实际输出确是1>0>2>3>6
.
有人解释说,这是由于从执行延时1ms的延时函数,到执行0ms
的延时函数,中间超过了1ms
,致使延时1ms
的回调函数先于延时0ms
的回调函数进入task queue
中。可是这种说法真不能苟同,若是这样都须要1ms
那么js
的运行效率也过低了。并且能够在1ms
的延时函数 和0ms
的延时函数打印时间戳,能够发现,根本不多是运行超过1ms
致使的结果。
那么咱们将1ms
的延时函数和0ms
的延时函数任意交换位置能够发现,谁在前面谁先进入task queue
。那么能够大胆推论,其实延时0ms
与延时1ms
是等价的(这个结论是自我推导的,不必定正确)。所以才有了1>0>2>3>6
输出顺序。
有人会说上面例子输出结果是由于setTimeout
的最小间隔时长致使的。最小间隔时长,是个很恶心的概念,最初接触的时候没有正确理解,致使一度认为这个最小间隔时长有问题。
咱们来看看最小间隔时长在MDN
上面的解释。在MDN
上面它不叫“最小间隔时长”,而是叫作“最小延迟时间”。在之前,最小间隔时长一般为10ms
,如今的现代浏览器一般为4ms
(根据各个浏览器的不一样会有些差别)。一直以来,都被“最小延迟时间”这个名词所误导,总认为延时时长必须大于等于最小延迟时间。可是,各类测试老是实现不了或者验证不了这个“最小延迟时间”。在读MDN
的时候,发现它有这么一句话"这一般是因为函数嵌套致使(嵌套层级达到必定深度),或者是因为已经执行的setInterval
的回调函数阻塞致使".
在浏览器中,setTimeout()/setInterval() 的每调用一次定时器的最小间隔是4ms,这一般是因为函数嵌套致使(嵌套层级达到必定深度),或者是因为已经执行的setInterval的回调函数阻塞致使的
这才焕然大悟,原来“最小延迟时间”是有限制条件的,他的限制条件就是函数嵌套到达必定深度,或者setInterval回调阻塞。
那么接下来咱们就用一段代码验证下:
function doFunc(count){
console.time('time total:')
let timeFunc = function(){
if(count>=0){
setTimeout(timeFunc,0)
count --;
}else{
console.timeEnd('time total:')
}
}
timeFunc()
}
doFunc(10)
复制代码
若是没有最小延迟时间的限制,那么在只有这段代码的环境下运行,那么应该会很快运行完。可是在chrome中实际输出确是33.2451171875ms
。这就直接证实了最小延迟时间的存在,而且触发他的条件是函数嵌套到达了必定深度。
好吧,我的觉的setTimeout
的小九九也就这些了,没有再写下去的必要了。好比setTimeout
回调函数中的this
,怎么清除当前创建的全部setTimeout
等等。相似这些地球人都知道的事情,估计也不是您看这篇文章的目的了。
走吧,客官们,嗨把刺激战场喽。。。。
一、《window.setTimeout》developer.mozilla.org/zh-CN/docs/…
二、《JavaScript 运行机制详解:再谈Event Loop》www.ruanyifeng.com/blog/2014/1…
三、《setTimeout最小间隔4ms的问题》segmentfault.com/q/101000001…
四、《setTimeout初探(一):4ms的真伪》 blog.csdn.net/yiifaa/arti…
五、 《setTimeout的那些事》imweb.io/topic/56ac6…