手机版

JavaScript运行机制事件循环详解

时间:2021-09-09 来源:互联网 编辑:宝哥软件园 浏览:

1.为什么JavaScript是单线程的?

JavaScript语言的特点之一是单线程,这意味着一次只能做一件事。那么为什么JavaScript不能有多线程呢?这样可以提高效率。

JavaScript的单线程与其用途有关。作为一种浏览器脚本语言,JavaScript主要用于与用户交互和操作DOM。这就决定了它只能是单线程,否则会带来复杂的同步问题。例如,假设JavaScript同时有两个线程,一个线程向DOM节点添加内容,另一个线程删除节点。浏览器应该以哪个线程为标准?因此,为了避免复杂性,JavaScript从诞生之日起就一直是单线程的,这已经成为这种语言的核心特性,未来也不会改变。

为了利用多核CPU的计算能力,HTML5提出了Web Worker标准,允许JavaScript脚本创建多个线程,但子线程完全由主线程控制,无法操作DOM。因此,这个新标准并没有改变JavaScript单线程的本质。

第二,任务队列

单线程意味着所有任务都需要排队,上一个任务完成后,下一个任务才会执行。如果前一个任务需要很长时间,那么后一个任务将不得不一直等待。

如果队列是因为计算量大,CPU太忙,那就算了,但大部分时间CPU都是空闲的,因为IO设备(输入输出设备)速度慢(比如Ajax操作从网络读取数据),要等结果出来才能执行。

JavaScript的设计者意识到CPU可以挂起等待的任务,不管IO设备如何,先运行后面的任务。当IO设备返回结果时,返回并继续执行挂起的任务。

因此,JavaScript有两种执行模式:一种是CPU按顺序执行,前一个任务结束后再执行下一个任务,称为同步执行;另一种是CPU跳过等待时间较长的任务,先处理后面的任务,称为异步执行。程序员选择自己的执行模式。

具体来说,异步执行的运行机制如下。(同步执行也是如此,因为没有异步任务就可以认为是异步执行。)

(1)所有任务在主线程上执行,形成执行上下文堆栈。(2)主路由之外还有一个“任务队列”。系统将异步任务放入“任务队列”,然后继续执行后续任务。(3)一旦“执行栈”中的所有任务被执行,系统将读取“任务队列”。如果异步任务此时已经完成等待,它将从“任务队列”进入执行堆栈并继续执行。(4)主线程重复上面的第三步。

下图是主线程和任务队列的示意图。

只要主线程为空,就会读取‘任务队列’,这是JavaScript的运行机制。这个过程将会重复。

三.事件和回调函数

任务队列本质上是一个事件队列(也可以理解为一个消息队列)。当一个输入输出设备完成一个任务时,一个事件被添加到任务队列中,表明相关的异步任务可以进入执行堆栈。主线程读取“任务队列”,也就是读取队列中的事件。

“任务队列”中的事件包括一些用户生成的事件(如鼠标点击、页面滚动等)。)除了IO设备的事件。只要指定了回调函数,这些事件就会在发生时进入‘任务队列’,等待主线程读取。

所谓的“回调函数”是将被主线程挂起的代码。异步任务必须指定回调函数,这些函数将在异步任务从“任务队列”返回执行堆栈时执行。

“任务队列”是一种先进先出的数据结构,排在前面的事件会先返回到主线程。主线程的读取过程基本上是自动的。一旦执行堆栈被清空,“任务队列”中的第一个事件将自动返回到主线程。但是由于后面提到的‘timer’函数,主线程要检查执行时间,有些事件必须在指定的时间返回给主线程。

四.事件循环

主线程从‘任务队列’中读取事件,这个过程是无止境的,所以整个运行机制也被称为Event Loop。

为了更好地理解事件循环,请参见下图(引自菲利普罗伯茨讲座《Help, I'm stuck in an event-loop》)。

在上图中,当主线程运行时,会生成一个堆和一个栈,栈中的代码会调用各种外部API,这些API会将各种事件(click、load、done)添加到‘任务队列’中。只要执行堆栈中的代码,主线程就会读取“任务队列”,并依次执行与这些事件对应的回调函数。

执行堆栈中的代码总是在读取“任务队列”之前执行。请看下面的例子。复制代码如下: var req=new XMLHttpRequest();req.open('GET ',URL);req . onload=function(){ };req . onerror=function(){ };req . send();上面代码中的req.send方法是将数据发送到服务器的Ajax操作,这是一个异步任务,这意味着在当前脚本的所有代码都执行完之前,系统不会读取‘任务队列’。因此,它相当于下面的写作。复制代码如下: var req=new XMLHttpRequest();req.open('GET ',URL);req . send();req . onload=function(){ };req . onerror=function(){ };也就是说,指定的回调函数(onload和onerror)是在send()方法之前还是之后并不重要,因为它们是执行栈的一部分,系统总是会在读取‘任务队列’之前完成执行。

动词(verb的缩写)计时器

除了放置异步任务,‘任务队列’还有另一个功能,就是可以放置定时事件,也就是指定一些代码执行后的时间。这被称为‘定时器’(timer)函数,即定期执行的代码。

定时器功能主要由setTimeout()和setInterval()完成。它们的内部运行机制完全相同,不同的是前者指定的代码执行一次,而后者重复执行。下面主要讨论setTimeout()。

SetTimeout()取两个参数,第一个是回调函数,第二个是延迟执行的毫秒数。

复制代码如下: console . log(1);setTimeout(function(){ console . log(2);},1000);console . log(3);

上面代码的执行结果是1,3,2,因为setTimeout()将第二行的执行延迟了1000毫秒。

如果setTimeout()的第二个参数设置为0,则意味着指定的回调函数将在当前代码执行(堆栈清空)后立即执行(0毫秒间隔)。

复制代码如下: settimeout(function(){ console . log(1);}, 0);console . log(2);

上面代码的执行结果总是2,1,因为‘任务队列’中的回调函数直到第二行执行完才会执行。按照HTML5标准,setTimeout()第二个参数的最小值(最短间隔)不得小于4毫秒,如果低于这个值,就会自动增加。在此之前,在旧浏览器中最短的时间间隔被设置为10毫秒。

此外,对于那些DOM更改(尤其是涉及页面重新呈现的更改),它们通常不会立即执行,而是每16毫秒执行一次。requestAnimationFrame()的效果比setTimeout()好。

需要注意的是,setTimeout()只在‘任务队列’中插入事件,主线程在执行当前代码(执行栈)之前不会执行它指定的回调函数。如果当前代码需要很长时间,可能需要等待很长时间,因此不能保证回调函数会在setTimeout()指定的时间执行。

不及物动词节点的事件循环

Node.js也是单线程的Event Loop,但是它的运行机制和浏览器环境不同。

请看下图(作者@BusyRich)。

根据上图,Node.js的运行机制如下。

(1)1)V8引擎解析JavaScript脚本。(2)解析后的代码调用节点API。(3)libuv库负责Node API的执行。它将不同的任务分配给不同的线程,形成一个Event Loop,并将任务的执行结果以异步方式返回给V8引擎。(4)V8引擎将结果返回给用户。

除了setTimeout和setInterval,Node.js还提供了另外两个与“任务队列”相关的方法:process.nextTick和setImmediate。它们可以帮助我们加深对“任务队列”的理解。

process.nextTick方法可以在当前“执行堆栈”的末尾触发回调函数——在主线程下次读取“任务队列”之前。也就是说,它指定的任务总是发生在所有异步任务之前。SetImmediate方法在当前‘任务队列’的末尾触发回调函数,也就是说,它指定的任务总是在主线程下次读取‘任务队列’时执行,类似于setTimeout(fn,0)。参见以下示例(通过StackOverflow)。复制代码如下:process.nexttick(函数a(){ console . log(1);process.nextTick(函数B(){ console . log(2);});});

setTimeout(函数超时(){ console . log(' time out FILEED '));},0)//1//2//TIMEOUT fireed在上面的代码中,由于process.nextTick方法指定的回调函数总是在当前‘执行栈’的末尾触发,不仅函数A在settimeout指定的回调函数超时之前执行,函数B也在time out之前执行。这意味着,如果有多个process.nextTick语句(无论它们是否嵌套),它们都将在当前的“执行堆栈”中执行。

现在,让我们看看setImmediate。复制代码如下:set immediate(函数a(){ console . log(1);setImmediate(函数B(){ console . log(2));});});

setTimeout(函数超时(){ console . log(' time out FILEED '));},0)//1//超时发射//2

在上面的代码中,有两个setImmediate。第一个setImmediate指定在当前“任务队列”(下一个“事件周期”)结束时触发回调函数a;然后,setTimeout还指定在当前“任务队列”的末尾触发回调函数超时,因此在输出结果中,TIMEOUT FIRED排在1之后。至于TIMEOUT FIRED后面的第二行,是因为setImmediate的另一个重要特性:一个‘事件循环’只能触发setImmediate指定的回调函数。

因此,我们得到了一个重要的区别:多个process.nextTick语句总是执行一次,而多个setImmediate语句需要执行几次。其实这就是Node.js版本增加setImmediate方法的原因,否则像下面这样递归调用process.nextTick会没完没了,主线程根本不会读取‘事件队列’!复制代码如下:process。nexttick (function foo () {process。next tick(foo);});实际上,如果您现在编写一个递归进程,Node.js将抛出一个警告,要求您将其更改为setImmediate。另外,由于process.nextTick指定的回调函数是在这个‘事件周期’触发的,setImmediate是在下一个‘事件周期’触发的,显然前者总是比后者发生的早,执行效率也高(因为不需要检查‘任务队列’)。

版权声明:JavaScript运行机制事件循环详解是由宝哥软件园云端程序自动收集整理而来。如果本文侵犯了你的权益,请联系本站底部QQ或者邮箱删除。