浏览器工作原理(16) - setTimeout实现原理

上一篇文章讲了消息队列和事件循环的有关知识,同时引出了微任务和宏任务的概念,这篇文章继续往下面说,这次来一起看看setTimeout这个函数

相信各位在平时开发的过程中,经常会用到这个setTimeout函数,并且觉得也不是很难,就是一个定时器,用来指定在多少毫秒之后执行某个函数,返回的是一个编号,代表这个setTimeout定时器,编号的作用是用来清除该定时器的时候用,基本用法如下代码:

function showName(){
  console.log("小明")
}
var timerID = setTimeout(showName,200);

这篇文章不教大家怎么用,而是从浏览器的工作原理来分析一下setTimeout函数是如何实现的

浏览器怎么实现 setTimeout

先来回顾一下之前讲过的事件循环系统,渲染进程中所有运行在主线程上的任务都需要先添加到消息队列,然后事件循环系统再按照顺序执行消息消息队列中的任务。看一个案例:

  • 接收到 HTML 文档数据,渲染引擎将“解析 DOM”事件添加到消息队列中
  • 当用户改变了 Web 页面的窗口大小,渲染引擎就会将“重新布局”的事件添加到消息队列中。
  • 当触发了 JavaScript 引擎垃圾回收机制,渲染引擎会将“垃圾回收”任务添加到消息队列中
  • 同样,如果要执行一段异步 JavaScript 代码,也是需要将执行任务添加到消息队列中

这些事件存放在消息队列之后,事件循环系统就会按照消息队列中顺序来执行事件

要执行一段异步任务,需要先将任务添加到消息队列中,但是用定时器设置的回调函数不太一样,需要在指定的时间间隔内被调用,但消息队列中的任务是按照顺序来执行的,所以并不能将定时器的回调函数直接放在消息队列中

那么该如何设计定时器,才能让任务在规定的时间之后执行呢?

其实在chrome中除了正常使用的消息队列之外,还有一个消息队列,里面维护的都是需要延迟执行的任务列表,比如setTimeout这个任务,创建的定时器,渲染进程会将该定时器的回调任务添加到延迟队列中。

当JavaScript调用setTimeout设置回调函数的时候,渲染进程会创建一个回调任务,包含了回调函数,当前发起时间,延迟执行时间等信息,然后加入到延迟队列中。


加入到了队列中,然后就是如何调用的问题了

翻阅资料会看到,循环执行消息队列的函数中,是先执行普通任务,然后执行延迟任务,也就是说一个普通的任务执行完之后,会根据发起时间和延迟时间计算出开始执行的时间,然后依次执行延迟任务。设置定时器的时候,同时会返回一个定时器ID,取消定时器的方法是使用clearTimeout函数,原理是chrome会把该任务从任务队列中删除

使用setTimeout的一些注意事项

在使用定时器的过程中,如果你不了解定时器的一些细节,那么很有可能掉进定时器的一些陷阱里。下面看一下都有哪些需要注意的地方

1、如果当前任务执行很久,会影响定时器任务的执行

看下面的代码:


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()

定时器本来意图是延迟0毫秒执行,但是for循环里计算很多,势必会影响bar函数的执行,可以看一下chrome的performance执行过程

在这里插入图片描述

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

也就是说在定时器函数里面嵌套调用定时器,也会延迟定时器的执行时间,看下面的代码:

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

在这里插入图片描述
从图中可以看到,前面五次调用的时间间隔比较小,嵌套调用超过五次以上,后面每次的调用最小时间间隔是 4 毫秒。之所以出现这样的情况,是因为在 Chrome 中,定时器被嵌套调用 5 次以上,系统会判断该函数方法被阻塞了,如果定时器的调用时间间隔小于 4 毫秒,那么浏览器会将每次调用的时间间隔设置为 4 毫秒

3、 未激活的页面,setTimeout 执行最小间隔是 1000 毫秒

未被激活的页面中定时器最小值大于 1000 毫秒,也就是说,如果标签不是当前的激活标签,那么定时器最小的时间间隔是 1000 毫秒,目的是为了优化后台页面的加载损耗以及降低耗电量。这一点你在使用定时器的时候要注意。

4. 延时执行时间有最大值

除了要了解定时器的回调函数时间比实际设定值要延后之外,还有一点需要注意下,那就是 Chrome、Safari、Firefox 都是以 32 个 bit 来存储延时值的,32bit 最大只能存放的数字是 2147483647 毫秒,这就意味着,如果 setTimeout 设置的延迟值大于 2147483647 毫秒(大约 24.8 天)时就会溢出,那么相当于延时值被设置为 0 了,这导致定时器会被立即执行

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

这个应该是老生常谈的话题了,如果被 setTimeout 推迟执行的回调函数是某个对象的方法,那么该方法中的 this 关键字将指向全局环境,而不是定义时所在的那个对象


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

这里输出的是 1,因为这段代码在编译的时候,执行上下文中的 this 会被设置为全局 window,如果是严格模式,会被设置为 undefined。

针对这个问题,有两个解决办法:

第一种是将MyObj.showName放在匿名函数中执行,如下所示:


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

第二种是使用 bind 方法,将 showName 绑定在 MyObj 上面,代码如下所示:


setTimeout(MyObj.showName.bind(MyObj), 1000)
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值