首页>>前端>>JavaScript->从setTimeout了解JS函数的执行时机

从setTimeout了解JS函数的执行时机

时间:2023-11-30 本站 点击:0

前置知识

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的异步特性,函数执行的时机不同,最后输出的结果也可能会不同。尤其是异步函数,因为被调用的时间和实际完整地在主线程中执行完毕的时间不一致,这个过程中异步函数涉及到的变量可能早已发生变化,输出结果可能不符合我们这些初学者的预期。

典型例子

letifor(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(leti=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的方法

用带参函数实现参数传递

letifunctiona(i){setTimeout(function(){console.log(i)},0);}for(i=0;i<6;i++){a.call(undefined,i);}

将回调函数写成立即执行函数

letifor(i=0;i<6;i++){setTimeout((()=>{console.log(i)})(),0)}


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:/JavaScript/3918.html