在前面,我们介绍过一个异步的概念,所谓异步,就是函数不会立刻执行,需要等到某个时间点才会执行,这就是异步。例如 setTimeout 中的回调函数,这个函数会在一秒钟后执行,这其实就是异步。同样,事件处理程序,例如点击事件,是需要等待用户点击之后才会运行的函数,这种函数也叫作异步函数。
我们看下面的例子:
js
console.log(1);
setTimeout(function(){
console.log(2);
},1000);
console.log(3);这个函数会输出 1,3,2,相信这个问题难不倒读者。
js
console.log(1);
setTimeout(function(){
console.log(2);
},0);
console.log(3);但是如果我把这个时间改成 0,结果又如何呢?读者会发现,哪怕延迟 0 毫秒触发,他好像也不是立刻执行的,输出结果也会是 1,3,2。下面我们再极端一点:
js
console.log(1);
setTimeout(function(){
console.log(2);
},0);
for(var i = 0;i<100000;i++){
console.log(i);
}读者可以自行运行一下这个代码,读者会发现,哪怕十万次循环会卡一秒钟,但是这个 2 依旧是最后输出的。所以其实这个异步代码其实始终是会在同步代码运行完毕之后才会执行的。
要解释清楚这个问题其实比较麻烦,首先,在浏览器环境中,有如下五个线程
- JS 执行引擎:负责执行 JS 代码
- 渲染线程:负责渲染页面
- 计时器线程:专门负责计时
- 事件监听线程:负责监听事件
- http网络线程:负责网络通信
当我们调用计时器的时候,JS 线程其实是会通知计时器线程,然后紧接着继续执行,这其实也就解释了为什么计时这个操作不会导致 JS 阻塞的原因。
事件队列:一块内存空间专门用于存放执行时机到达的异步函数。
当在 JS 执行过程中,调用了计时器的时候,JS 线程会通知计时器线程,告诉计时器线程将会在指定时间后执行这个函数,这个时候 JS 引擎会自动继续执行,计时器在等到指定时间之后,他会把这个函数放到事件队列里面去。实际上我们还知道,一个函数的执行是依赖于执行上下文栈的,只要 JS 引擎在执行,那么执行上下文栈中就一定有东西,所以实际上,只有当 JS 的执行上下文栈为空的时候,才会去把事件队列里面的函数一个一个拿到执行上下文栈中执行。
这样就可以解释上面的代码了,输出 2 的函数本质上是一个异步函数,异步函数是要丢给计时器线程的,哪怕 0 秒也得丢给计时器线程,计时器线程计时结束之后会把这个函数放到事件队列里面去,发现问题了吧,不管是输出 1 还是后面的一大堆循环,本质上都是同步代码的内容,是直接就在 JS 执行上下文中运行的内容,而只有等 JS 引擎空闲的时候,也就是执行上下文为空的时候,才会把事件队列里面的东西拿出来执行。
在 JS 中,只要执行上下文栈不为空,就会一直执行,所以在 JS 中,只要一个函数没有执行完,它一定不会被其他事件打断。