阅读 2248

你可能不知道的5个setTimeout冷知识

作为一名合格的程序员,想必大家对 setTimeout 并不陌生。它就是一个定时器,可以指定一个函数在多少毫秒后执行;它会返回一个定时器的编号,可以通过 clearTimeout 手动清除这个定时器。

在这里我不会重复介绍 setTimeout 是一个宏任务,是 JavaScript 执行异步函数的方法,也不会用它来实现一个符合 Promises/A+ 规范的 Promise。因为这些都太基础、太简单了!今天我要介绍的可能是你还不知道的 setTimeout 冷知识...(以下案例均在 Chrome 中执行,请您安心食用)

一、setTimeout 时间间隔自己涨了?

在我们平时写业务的时候可能会多次用到 setTimeout,那你有没有注意到当你创建一个 setTimeout 让它几毫秒后执行,这时你打开一个新的页面,让当前页签处于未激活状态时,其实它延迟变成了1秒钟

不妨我们做一个实验来验证一下,只需要打开你的 chrome 浏览器调出开发者模式,在 console 控制台输入以下代码,然后回车:

var timer = setTimeout(function fn() {
    timer = setTimeout(fn, 500)
    console.log(new Date().getSeconds())
}, 500)
复制代码

上面的代码是创建了一个循环定时器,每 500 毫秒执行一次一直循环下去。当你执行这段代码之后,它会再你的控制台中打印当前的时间秒单位,因为设置的是 500 毫秒间隔,不出意外的话,每个时间秒会打印两次。这时,你再打开一个新的tab页,让执行这段代码的tab页处于未激活的状态,心里默念几秒后,接下来就是见证奇迹的时刻。请切回到刚才的页面,看看在这期间控制台打印的时间秒发生了什么变化!

好了,我们可以清掉它了! clearTimeout(timer) 使用 setInterval 也会得到同样效果。

二、setTimeout 什么时候执行不取决于我设置的时间?

先看代码:

function fn() {
    setTimeout(() => {
        console.log('go')
    }, 1)
}
复制代码

当我们执行 fn 函数时,再1毫秒后会打印出 go. 那么接下来我们改动一下这个方法.

function fn() {
    for (var i = 0; i < 10000; i++) {
       console.log(i)
    }
    setTimeout(() => {
        console.log('go')
    }, 1)
}
复制代码

我们再执行一下上面的代码,go 还会立刻在1毫秒后打印吗?

知道的同学可以略过,不太了解或者是对概念很模糊讲不清楚的同学,建议看完这段。这里我们系统的复习一下知识点,尽量给大家讲清楚原因。

众所周知,在浏览器中JavaScript 是单线程的,这里会有同学产生疑问,HTML5提出Web Worker标准,允许JavaScript 脚本创建多个线程。这里的多线程由一个主线程和多个子线程组成的,子线程完全受控于主线程。所以我们依然可以认为它还是单线程。

那么单线程有什么特点呢?就是大家要做什么事情,都需要排队。这里就需要说到一个重要的概念-消息队列

消息队列是一种数据结构,可以存放要执行的任务。它符合队列先进先出的特点,新的任务添加到队列的尾部,主线程会循环地从消息队列头部中读取任务并执行任务。可以把它想象成一条货物传送带,一个人从一头儿不断的把货物放在传送带上,在另一头儿有一个人不断的从传送带上把货物取下来。

消息队列中的任务包括渲染事件、用户交互事件、脚本执行事件、网络请求完成、文件读写完成事件和定时器等等。而我们今天主要介绍定时器,定时器是通过异步回调函数封装成一个宏任务,然后把他添加到消息队列的尾部,如果有多个定时器消息队列是怎么安排的呢?大家可以这么理解,一些异步执行的任务都存放在另一个特殊的消息队列,这个特殊的消息队列里存放着所有异步任务,我们可以称它为异步消息队列,这个异步消息队列里的任务会在主消息队列中的所有任务执行完之后它再执行。

这样就可以说的通上面的代码中 setTimeout 为什么没有准时在1毫秒后执行了。因为for循环还在占着主线程并没有执行完,好比传送带上的货物太沉太大,一时半会儿拿不下来,不管后面是谁都得给我等着。

三、setTimeout 嵌套最少间隔4毫秒

上面我们有用 setTimeout 写了一个循环定时器。每 500 毫秒执行一次一直循环下去。这里可设置的最小时间为 4 毫秒,如果你设置小于 4 毫秒,那么它默认会 4毫秒执行一次。

var timer = setTimeout(function a() {
    timer = setTimeout(a, 2)
    console.log(new Date().getMilliseconds())
}, 2)
复制代码

其实在前 5 次执行中时间间隔会是 2 毫秒,后面每次的调用最小时间间隔是 4 毫秒。之所以出现这样的情况,是因为在 Chrome 中,定时器被嵌套调用 5 次以上,系统会判断该函数方法被阻塞了,如果定时器的调用时间间隔小于 4 毫秒,那么浏览器会将每次调用的时间间隔设置为 4 毫秒。

四、setTimeout 延时执行时间有最大值

Chrome、Safari 和 Firefox 浏览器都是以 32 个 bit 来存储延时值的,32bit 最大只能存放的数字是 2147483647,换算一下相当于 24小时51分18秒。那么这就意味着 setTimeout 设置的延迟值大于 2147483647 毫秒就会溢出。

function fn() {
    setTimeout(() => {
        console.log('go')
    }, 2147483648)
}
复制代码

如上代码, 执行 fn 函数会立刻打印。

五、setTimeout 1 不一定大于 0

为了这么讲呢?先看代码:

function fn() {
    setTimeout(() => {
        console.log('go')
    }, 1)
    
    setTimeout(() => {
        console.log('on')
    }, 0)
}
复制代码

如上代码, 执行 fn 函数打印顺序是什么呢?凭直觉第一个函数1毫秒后执行,第二个函数0毫秒后执行,那么就是先打印 on 后打印 go。但是结果并非如此。

总结

到这里可以看出 setTimeout 本身就略微带一点任性,如果你对它不是特别的了解,在项目中不到万不得已还是尽量少用它,因为它的迷惑性太大。

最后附上一道面试题,考考大家对 setTimeout 的理解够不够深。

for (var i = 0; i < 10; i++) {
   setTimeout(() => {
     console.log(i)
   })
}
复制代码

执行上面会输出什么?想必难不倒大家,那么再问如果只允许改变 for 循环内方法体如何让它输出0到9呢?

5秒后揭晓答案.....

for (var i = 0; i < 10; i++) {
// setTimeout(() => {
     console.log(i)
// })
}
复制代码

关注下面的标签,发现更多相似文章
评论