你真的了解setTimeout吗?

630 阅读5分钟

上来就是灵魂发问......

有同学说,setTimeout是定时器的一个方法,用来指定某个函数在多少毫秒之后执行。

比如下边这段代码

function sayHello(){
   console.log('hello world')
}

let timer1 = setTimeout(sayHello(),1000)

这段代码执行的结果就是,在1000毫秒后执行sayHello(),即在控制台上显示"hello world"

看到这的同学一脸疑惑,就这??也没有什么是我么不懂得呀

那么接下来就有了这段代码

const useTime = t => {
  let start = Date.now()
  while((Date.now() - start) < t){}
}

let timer1 = setTimeout(() => {
  console.log(3)
},500)

let timer2 = setTimeout(() => {
  console.log(4)
},1000)

console.log(1)
useTime(2000)
console.log(2)

这还不简单,3 4 1 2,有同学脱口而出~

那么我们运行一下

可能很多像我一样的初学者感到疑惑,不应该是按顺序输出3 4 1 2吗?1 2之间可能还要间隔2000ms。

在讲这个问题之前,有关于javascript的一些知识需要跟大家分享一下。

单线程和任务队列

我们知道JS是单线程的语言,什么是单线程呢?

简单来说,就是在同一时间你只能做一件事,那么为什么JS不像语言一样使用更高效率的多线程呢?这是由JS的作用决定的,从诞生之初,它就是一门为了增强用户互动的脚本语言。

比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?

所以,最终JS的设计者们,规定了JS是单线程。

前边提到了单线程意味着所有的任务需要排队,也就是说只有执行完上一个任务,下一个任务才会开始执行,这就使得用户体验很不好,毕竟大家谁都不能接受网页一个字一个字的往外蹦(玩笑话)。

举一个简单的例子,当你早晨起床的时候想吃一桶泡面,但泡好需要10min(疯狂夸张),那么相信各位没人会傻傻陪泡面一起泡,而是先去刷牙、洗脸,等你洗漱完,泡面也喷喷儿香的在等你了。

这就揭示了一个简单的问题,如果任务排队是因为计算量很大,CPU忙不过来,倒也算了,但是很多时候CPU是闲着的,因为IO设备(输入输出设备)很慢,不得不等着结果出来,再往下执行。

所以JS的设计者们一样拥有聪明的脑瓜,他们意识到主线程完全可以先不管IO设备,挂起一些需要等待的结果,等主线程运行完,IO设备返回了结果,再回过头来把这些任务继续执行下去。

于是任务就可以分为两种,一种是同步任务:简单来说就是主线程上的任务,这些任务只有上一个执行完了才会执行下一个,如果上一个陷入死循环出不来了,那下一个永远不会执行;另一种是异步任务,指的是不进入主线程而是进入任务队列的任务,常见的异步任务有如下几种

回到我们刚刚的例子

计时器就属于异步任务的一种,也就是当代码开始运行时,遇到函数声明不管,先碰到的可执行的代码是timer1,由于它是异步任务,所以挂在一边让他自己玩去;然后碰到了第二个可执行代码timer2,同样的,也是放在一边;接下来是console.log(1),所以立马输出1;遇到函数,需要执行2000ms;接下来,执行代码console.log(2),输出2。这时放在旁边的两段代码已经执行完毕,并且将他们的结果放在了任务队列里,此时主线程继续执行任务队列的任务,并同步返回3和4.

图例如下

总结一下,就是下边的过程:

(1)所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
(2)主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
(3)一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
(4)主线程不断重复上面的第三步。

接下来做几个例题巩固一下

setTimeout(()=>{
  console.log(1)
}, 0)
console.log(2)

输出结果是2 1 这里有一个知识需要补充 延时0ms和延时1ms效果其实是一样的,定时器有最小时间粒度

let t = true
setTimeout(() => {
   t = false
}, 1000)
while(t){ }
console.log('end')

这里刚开始没有东西在控制台上输出,1000ms后,控制台上输出end。

是吗?当然不是,划重点,我们前边提到了主线程如果陷入了死循环,任务队列的语句就不会执行了,从始至终只有主线程一个人在做事,他现在盗梦空间第五层出不来了,当然也没人去照顾它的小女儿,也就是说,任务队列没有被执行,最终控制台上啥也么得!!!

\

参考资料