Javascript引擎的单线程机制和setTimeout执行原理阐述

2023-06-12,,

工作中使用setTimeout解决了一个问题,于是对setTimeout的相关资料整理了下,以及对js引擎执行的原理一并整理了下,希望能给码农们一些帮助。若发现有错的地方大家及时指出,共同学习进步。

一、首先对js的单线程运行机制做一个整理;先来一张图片,直观的感受下浏览器中js是如何使用单线程机制对同步、异步函数,以及鼠标的单击事件、浏览器触发事件、Timer定时器事件(setTimeout函数)、Interval间隔执行事件(setInterval函数)的执行顺序;

对上图js主线程的运行机制进一步阐述说明:

a、所有同步任务都在主线程上执行,形成一个js的执行栈;

b、主线程之外,还存在一个"任务队列"。当异步任务有了运行结果,就往"任务队列"之中放置一个回调事件,setTimeout、setInterval等都会将回调事件塞进队列中;

c、一旦"执行栈"中的所有同步任务执行完毕,主线程就会读取"任务队列",并且按顺序依次将队列中的事件放进栈中执行,原本队列中处于等待阻塞状态的函数开始执行;

d、主线程不断重复运行c,直到队列中的函数全部运行完。

二、JavaScript引擎用单线程运行是有意义的,单线程不必理会线程同步这些复杂的问题,那么单线程的JavaScript引擎是怎么配合浏览器内核处理这些定时器和响应浏览器事件的呢?下面结合浏览器内核处理方式简单说明。浏览器内核实现允许多个线程异步执行,这些线程在内核制控下相互配合以保持同步。

假如某一浏览器内核的实现至少有三个常驻线程:javascript引擎线程、界面渲染线程、浏览器事件触发线程,除些以外也有一些执行完就终止的线程,如Http请求线程,这些异步线程都会产生不同的异步事件。下面通过一个图来阐明单线程的JavaScript引擎与另外那些线程是怎样互动通信的。虽然每个浏览器内核实现细节不同,但这其中的 调用原理都是大同小异。

由图可看出:浏览器中的JavaScript引擎是基于事件驱动的,这里的事件可看作是浏览器派给它的各种任务,这些任务可以源自 JavaScript引擎当前执行的代码块,如调用setTimeout添加一个任务,也可来自浏览器内核的其它线程,如界面元素鼠标点击事件,定时触发器时间到达通知,异步请求状态变更通知等。从代码角度看来任务实体就是各种回调函数,JavaScript引擎一直等待着任务队列中任务的到来。由于单线程关系,这些任务要进行排队一个接着一个被引擎处理。

三、setTimeout执行原理说明:setTimeout设置的延迟执行时间是不能被保证的,就是说你这样写setTimeout(fn, 500)并不代表fn肯定在500毫秒之后马上就执行,延迟很可能会更长。因为 JavaScript 是单线程语言,所有的异步事件(包括计时器、鼠标事件或者一个 XMLHttpRequest 完成)仅仅当程序执行期间有缺口的时候才会执行,不是你规定了什么时候就什么时候执行,最终还是要以浏览器确定。

浏览器的更新间隔也是会影响延迟的时间,例如:IE8及其之前的IE版本更新间隔为15.6毫秒。假设你设定的setTimeout延迟为16.7ms,那么它要更新两个15.6毫秒才会该触发延时。这也意味着无故延迟了 15.6 x 2 - 16.7 = 14.5毫秒。下图以矢量表示:

16.7ms
DELAY: |------------|

CLOCK: |----------|----------|
15.6ms 15.6ms
所以即使你给setTimeout设定的延时为0ms,它也不会立即触发。目前Chrome与IE9+浏览器的更新频率都为4ms(如果你使用的是笔记本电脑,并且在使用电池而非电源的模式下,为了节省资源,浏览器会将更新频率切换至于系统时间相同,也就意味着更新频率更低)。退一步说,假使timer resolution能够达到16.7ms,它还要面临一个异步队列的问题。因为异步的关系setTimeout中的回调函数并非立即执行,而是需要加入等待队列中。但问题是,如果在等待延迟触发的过程中,有新的同步脚本需要执行,那么同步脚本不会排在timer的回调之后,而是立即执行。下图是对上述说明的直观展示:

setTimeout和setInterval之间的区别:setTimeout 和 setInterval 在执行异步代码的时候有着根本的不同,如果一个计时器被阻塞而不能立即执行,它将延迟执行直到下一次可能执行的时间点才被执行(比期望的时间间隔要长些);如果setInterval回调函数的执行时间将足够长(比指定的时间间隔长),它们将连续执行并且彼此之间没有时间间隔。

举个例子:

setTimeout(function(){
/* Some long block of code... */
setTimeout(function(){...}, 10);
}, 10);

setInterval(function(){
/* Some long block of code... */
}, 10);

setTimeout回调函数的执行和上一次执行之间的间隔至少有10ms(可能会更多,但不会少于10ms),而setInterval的回调函数将尝试每隔10ms执行一次,不论上次是否执行完毕。

四、举例说明setTimeout的回调函数、双重求值

1、正常使用setTimeout函数的写法如下:

setTimeout( function () { console.log(1); } , 1000);
setTimeout( function () { console.log(2); } , 800);
setTimeout( function () { console.log(3); } , 600);

很显然打印的顺序如下图所示,根据每个函数的设置的时间,第三个函数最先进入任务队列,其次第二个函数第二,最后第一个函数最后进入。所以弹出3 2 1.

2、第二种情况,不构成回调函数

setTimeout( console.log(1) , 1000);
setTimeout( console.log(2) , 800);
setTimeout( console.log(3) , 600);

打印的顺序如下图所示:console.log(1) ,console.log(2),console.log(3)并不是构成回调函数,所以它们根本没有进入任务队列:而是按照正常的顺序在执行栈中被执行了。

3,、第三种就是今天要说的重要问题,setTimeout的双重求值

setTimeout( “console.log(1)“ , 1000);
setTimeout( “console.log(2)“ , 800);
setTimeout( “console.log(3)“ , 600);

JavaScript像其他其他语言一样,允许在程序中提取一个包含代码的字符串,然后动态执行。其中的eval()、Function()构造函数、setTimeout()和setInterval()都可以实现双重求值;

这四个函数是将传入的字符串当做JavaScript代码执行,这个被双引号包起来的代码段就类似于函数被执行。setTimeout(" ... ", 1000)的写法等价于setTimeout(function() { ... }, 1000)的写法。因为JavaScript编程语言拥有头等函数的特性,这意味着你可以将函数直接作为参数传递给其他接口,并将他们保存在变量中或者对象的属性中;在这样的语言中, 一个函数可以作为参数传递给其他函数,可以被当作返回值被另一个函数返回,可以当作值指定给一个变量。

总而言之,setTimeout是将第一个带双引号的参数当做被传递的函数使用了,所以执行结果就如上图所示了。

Javascript引擎的单线程机制和setTimeout执行原理阐述的相关教程结束。

《Javascript引擎的单线程机制和setTimeout执行原理阐述.doc》

下载本文的Word格式文档,以方便收藏与打印。