setTimeout()函数:用来指定某个函数或某段代码在多少毫秒之后执行。它返回一个整数,表示定时器timer的编号,可以用来取消该定时器。
例子
console.log(1); setTimeout(function () { console.log(2); }, 0); console.log(3);
问:最后的打印顺序是什么?(如果不了解js的运行机制就会答错)
正确答案:1 3 2
解析:无论setTimeout的执行时间是0还是1000,结果都是先输出3后输出2,这就是面试官常常考查的js运行机制的问题,接下来我们要引入一个概念,JavaScript 是单线程的。
二、 JavaScript 单线程
JavasScript引擎是基于事件驱动和单线程执行的,JS引擎一直等待着任务队列中任务的到来,然后加以处理,浏览器无论什么时候都只有一个JS线程在运行程序,即主线程。
通俗的说:JS在同一时间内只能做一件事,这也常被称为 “阻塞式执行”。
任务队列
那么单线程的JavasScript是怎么实现“非阻塞执行”呢?
答:异步容易实现非阻塞,所以在JavaScript中对于耗时的操作或者时间不确定的操作,使用异步就成了必然的选择。
诸如事件点击触发回调函数、ajax通信、计时器这种异步处理是如何实现的呢?
答:任务队列
所有任务可以分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。
任务队列:一个先进先出的队列,它里面存放着各种事件和任务。
同步任务
同步任务:在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务。
输出
如:console.log()
变量的声明
同步函数:如果在函数返回的时候,调用者就能够拿到预期的返回值或者看到预期的效果,那么这个函数就是同步的。
异步任务
setTimeout和setInterval
DOM事件
Promise
process.nextTick
fs.readFile
http.get
异步函数:如果在函数返回的时候,调用者还不能够得到预期结果,而是需要在将来通过一定的手段得到,那么这个函数就是异步的。
除此之外,任务队列又分为macro-task(宏任务)与micro-task(微任务),在ES5标准中,它们被分别称为task与job。
宏任务
I/O
setTimeout
setInterval
setImmdiate
requestAnimationFrame
微任务
process.nextTick
Promise
Promise.then
MutationObserver
宏任务和微任务的执行顺序
一次事件循环中,先执行宏任务队列里的一个任务,再把微任务队列里的所有任务执行完毕,再去宏任务队列取下一个宏任务执行。
注:在当前的微任务没有执行完成时,是不会执行下一个宏任务的。
三、setTimeout运行机制
setTimeout 和 setInterval的运行机制是将指定的代码移出本次执行,等到下一轮 Event Loop 时,再检查是否到了指定时间。如果到了,就执行对应的代码;如果不到,就等到再下一轮 Event Loop 时重新判断。
这意味着,setTimeout指定的代码,必须等到本次执行的所有同步代码都执行完,才会执行。
优先关系:异步任务要挂起,先执行同步任务,同步任务执行完毕才会响应异步任务。
四、进阶
console.log('A'); setTimeout(function () { console.log('B'); }, 0); while (1) {}
大家再猜一下这段程序输出的结果会是什么?
答:A
注:建议先注释掉while循环代码块的代码,执行后强制删除进程,不然会造成“假死”。
同步队列输出A之后,陷入while(true){}的死循环中,异步任务不会被执行。
类似的,有时addEventListener()方法监听点击事件click,用户点了某个按钮会卡死,就是因为当前JS正在处理同步队列,无法将click触发事件放入执行栈,不会执行,出现“假死”。
五、定时获取接口更新数据
for (var i = 0; i < 4; i++) { setTimeout(function () { console.log(i); }, 1000); }
输出结果为,隔1s后一起输出:4 4 4 4
for循环是一个同步任务,为什么连续输出四个4?
答:因为有队列插入的时间,即使执行时间从1000改成0,还是输出四个4。
那么这个问题是如何产生和解决的呢?请接着阅读
异步队列执行的时间
执行到异步任务的时候,会直接放到异步队列中吗?
答案是不一定的。