前置知识
JavaScript引擎是单线程的,一段JS代码只能在一个线程从上到下执行。
JS内置了一个setTimeout函数,用途是指定某个函数或者某段代码在多少毫秒之后执行。
疑问
console.log(1)
setTimeout(()=>{
console.log(2)
},1000)
console.log(3)
有这么一段代码,按照JS是单线程的说法,输出结果应该是1,过了1秒之后输出2,最后才输出3。然而事实是先输出1和3,过了1秒后输出2。
为什么单线程的JS,执行setTimeout()函数的时候下面的表达式不会被阻塞,而是无视setTimeout的运行状态,继续执行呢?
浏览器的线程
如果所有代码就放在JS主线程上执行,那么若setTimeout的延迟时间非常久,下面的代码一直等待setTimeout返回成功才继续执行,那么JS的效率可就太低了,所以为了解决这个问题,浏览器搞了几个其他线程辅助JS主线程的运行。
- GUI渲染线程
- JS引擎线程
- setTimeout定时器触发线程
- ...等等
其中JS引擎线程也就是主线程,就是运行JS代码的线程,setTimeout线程是异步线程。
任务队列
要实现非阻塞,主要靠异步,怎么实现呢,需要有一个静态的任务队列,存储异步处理完毕后返回的回调函数。
同步任务
在主线程上排队执行的任务,常见的有:
- 输出,如
console.log() - 变量声明
- 同步函数,也就是被调用时不会立即返回,而是等函数内所有任务都做完了再返回的函数。
异步任务
在异步线程上执行的任务,比如setTimeout,常见的还有AJAX等。
工作原理
一开始主线程执行console.log(1),紧接着检测到setTimeout()函数,把它移交给响应的异步线程处理,之后主线程就跳过setTimeout(),继续执行下面的console.log(3);一开始任务队列是空的,主线程中的setTimeout()进入异步线程后,开始执行,经过1秒钟的延迟后,setTimeout()的回调函数进入任务队列,主线程的同步任务执行完毕后,进入了空闲期,于是开始询问任务队列是否有任务需要主线程完成,此时任务队列里存在之前的setTimeout()运行完毕后的回调函数,这个函数被取出到主线程执行,于是回调函数中的console.log(2)内容被执行。(主线程、异步线程和任务队列中其实还有一个中介人叫轮询处理线程Event Loop,为了简化描述这里省略了)。
函数执行时机对输出结果的影响
从上面的描述可以知道,因为JS的异步特性,函数执行的时机不同,最后输出的结果也可能会不同。尤其是异步函数,因为被调用的时间和实际完整地在主线程中执行完毕的时间不一致,这个过程中异步函数涉及到的变量可能早已发生变化,输出结果可能不符合我们这些初学者的预期。
典型例子
let i
for(i=0;i<6;i++){
setTimeout(()=>{
console.log(i)
}
,0)
}
这段代码输出值为6 6 6 6 6 6,而不是0 1 2 3 4 5,原因就像上面说的,setTimeout()是异步函数,异步函数一开始并不在主线程中执行,主线程最先执行let i变量声明,然后执行for循环,for循环中每个循环执行一次异步函数setTimeout(),而这个异步函数移入setTimeout定时器触发线程,在这个异步线程中执行完毕后回调函数进入任务队列,一共6个循环,任务队列中就有6个回调函数console.log(i)等待主线程调用。6个循环结束后,同步任务for循环运行结束,主线程中没有要执行的任务了,就询问任务队列是否为空,任务队列中有6个console.log(i),依次按先入先出取出到主线程中执行,而这时i已经变成6了,所以输出6个6。
例外
for(let i=0;i<6;i++){
setTimeout(()=>{
console.log(i)
}
,0)
}
这段代码输出值不是6 6 6 6 6 6,而是0 1 2 3 4 5,原因是变量i放在了for循环参数列表中声明,这样做每次循环都会多创建一个i的副本,用来保存当前循环的i值,最后执行console.log(i)时,打印的不是i,而是前面i在不同阶段创建的副本,跟刻舟求剑有异曲同工之妙。
其他输出0 1 2 3 4 5的方法
用带参函数实现参数传递
let i
function a(i){
setTimeout(function(){
console.log(i)
},0);
}
for (i = 0; i < 6; i++) {
a.call(undefined,i);
}
将回调函数写成立即执行函数
let i
for(i=0;i<6;i++){
setTimeout((()=>{
console.log(i)
})(),0)
}