setTimeout是如何实现的?

介绍

setTimeout它就是 ⼀个定时器,⽤来指定某个函数在多少毫秒之后执⾏

它会返回⼀个整数,表⽰定时器的编号,同时你还可 以通过该编号来取消这个定时器。

示例:

function showName(){
console.log("极客时间")
}
var timerID = setTimeout(showName,200);


浏览器怎么实现setTimeout

我们知道渲染进程中所有运⾏在主线程 上的任务都需要先添加到消息队列,我们知道渲染进程中所有运⾏在主线程 上的任务都需要先添加到消息队列,例如:

  • 当接收到HTML⽂档数据,渲染引擎就会将“解析DOM”事件添加到消息队列中
  • 当⽤⼾改变了Web⻚⾯的窗⼝⼤⼩,渲染引擎就会将“重新布局”的事件添加到消息队列中。
  • 当触发了JavaScript引擎垃圾回收机制,渲染引擎会将“垃圾回收”任务添加到消息队列中。
  • 同样,如果要执⾏⼀段异步JavaScript代码,也是需要将执⾏任务添加到消息队列中。

在Chrome中除了正常使⽤的消息队列之外,还有另外⼀个消息队列,这个队列中维护了需要延迟执⾏的任务列表,包括了定时器和Chromium内部⼀些需要延迟执⾏的任务。所以当通过JavaScript创建⼀个定时器 时,渲染进程会将该定时器的回调任务添加到延迟队列中

当通过JavaScript调⽤setTimeout设置回调函数的时候,渲染进程将会创建⼀个回调任务,包含了回调函 数showName、当前发起时间、延迟执⾏时间

创建好回调任务之后,再将该任务添加到延迟执⾏队列中

浏览器会根据发起时间和延迟时间计算出到期的任务,然后依次 执⾏这些到期的任务


使⽤setTimeout的⼀些注意事项

如果当前任务执⾏时间过久,会影延迟到期定时器任务的执⾏

在使⽤setTimeout的时候,有很多因素会导致回调函数执⾏⽐设定的预期值要久,其中⼀个就是当前任务 执⾏时间过久从⽽导致定时器设置的任务被延后执⾏

function bar() {
console.log('bar')
}

function foo() {
setTimeout(bar, 0);
for (let i = 0; i < 5000; i++) {
let i = 5+8+8+8
console.log(i)
	}
}

foo()

在执⾏foo函数的时候使⽤setTimeout设置了⼀个0延时的回调任务,设置好回调任务后,foo 函数会继续执⾏5000次for循环。

通过setTimeout设置的回调任务被放⼊了消息队列中并且等待下⼀次执⾏,这⾥并不是⽴即执⾏的;要执 ⾏消息队列中的下个任务,需要等待当前的任务执⾏完成,由于当前这段代码要执⾏5000次的for循环,所 以当前这个任务的执⾏时间会⽐较久⼀点。这势必会影响到下个任务的执⾏时间。


如果setTimeout存在嵌套调⽤,那么系统会设置最短时间间隔为4毫秒

也就是说在定时器函数⾥⾯嵌套调⽤定时器,也会延⻓定时器的执⾏时间

function cb() { 
setTimeout(cb, 0); }

setTimeout(cb, 0);

在Chrome中,定时 器被嵌套调⽤5次以上,系统会判断该函数⽅法被阻塞了,如果定时器的调⽤时间间隔⼩于4毫秒,那么浏览 器会将每次调⽤的时间间隔设置为4毫秒。


未激活的⻚⾯,setTimeout执⾏最⼩间隔是1000毫秒

未被激活的⻚⾯中定时器最⼩值⼤于1000毫 秒,也就是说,如如果标签不是当前的激活标签,那么定时器最⼩的时间间隔是1000毫秒,⽬的是为了优化 后台⻚⾯的加载损耗以及降低耗电量。


延时执⾏时间有最⼤值

Chrome、 Safari、Firefox都是以32个bit来存储延时值的,32bit最⼤只能存放的数字是2147483647毫秒,这就意味 着,如果setTimeout设置的延迟值⼤于 2147483647毫秒(⼤约24.8天)时就会溢出,这导致定时器会被⽴ 即执⾏。

function showName(){
console.log("极客时间")
}

var timerID = setTimeout(showName,2147483648);//会被理解调⽤执⾏

这段代码是会⽴即被执⾏的


使⽤setTimeout设置的回调函数中的this不符合直觉

如果被setTimeout推迟执⾏的回调函数是某个对象的⽅法,那么该⽅法中的this关键字将指向全局环境,⽽ 不是定义时所在的那个对象。

var name= 1;

var MyObj = {
name: 2,
showName: function(){
console.log(this.name);
	}    
}

setTimeout(MyObj.showName,1000)

这⾥输出的是1,因为这段代码在编译的时候,执⾏上下⽂中的this会被设置为全局window

解决方法

第⼀种是将MyObj.showName放在匿名函数中执⾏

//箭头函数
setTimeout(() => {
MyObj.showName()
}, 1000);

//或者function函数
setTimeout(function() {
MyObj.showName();
}, 1000

第⼆种是使⽤bind⽅法,将showName绑定在MyObj上⾯

setTimeout(MyObj.showName.bind(MyObj), 1000)