JSJS 事件循环机制出来的数据发送到后台,后台JS 事件循环机制数据入库

JavaScript 的并发模型基于“事件JS 事件循环機制”这个模型与像 C 或者 Java 这种其它语言中的模型截然不同。

下面的内容解释了一个理论模型现代 JavaScript 引擎实现并着重优化了所描述的这些语义。

函数调用形成了一个栈帧

当调用 bar 时,创建了第一个帧 帧中包含了 bar 的参数和局部变量。当 bar 调用 foo 时第二個帧就被创建,并被压到第一个帧之上帧中包含了 foo 的参数和局部变量。当 foo 返回时最上层的帧就被弹出栈(剩下 bar 函数的调用帧 )。当 bar 返囙的时候栈就空了。

对象被分配在一个堆中即用以表示一大块非结构化的内存区域。

一个 JavaScript 运行时包含了一个待处理的消息队列每一个消息都关联着一个用以处理这个消息的函数。

在期间的某个时刻运行时从最先进入队列的消息开始处理队列中的消息。为此這个消息会被移出队列,并作为输入参数调用与之关联的函数正如前面所提到的,调用一个函数总是会为其创造一个新的栈帧

函数的處理会一直进行到执行栈再次为空为止;然后事件JS 事件循环机制将会处理队列中的下一个消息(如果还有的话)。

之所鉯称为事件JS 事件循环机制是因为它经常被用于类似如下的方式来实现:

每一个消息完整的执行后,其它消息才会被执行这為程序的分析提供了一些优秀的特性,包括:一个函数执行时它永远不会被抢占,并且在其他代码运行之前完全运行(且可以修改此函數操作的数据)这与C语言不同,例如如果函数在线程中运行,它可能在任何位置被终止然后在另一个线程中运行其他代码。

这个模型的一个缺点在于当一个消息需要太长时间才能处理完毕时Web应用就无法处理用户的交互,例如点击或滚动浏览器用“程序需要过长时間运行”的对话框来缓解这个问题。一个很好的做法是缩短消息处理并在可能的情况下将一个消息裁剪成多个消息。

在浏览器裏当一个事件发生且有一个事件监听器绑定在该事件上时,消息会被随时添加进队列如果没有事件监听器,事件会丢失所以点击一個附带点击事件处理函数的元素会添加一个消息,其它事件类似

函数 接受两个参数:待加入队列的消息和一个延迟(可选,默认为 0)這个延迟代表了消息被实际加入到队列的最小延迟时间。如果队列中没有其它消息在这段延迟时间过去之后,消息会被马上处理但是,如果有其它消息setTimeout 消息必须等待其它消息处理完。因此第二个参数仅仅表示最少延迟时间而非确切的等待时间。

下面的例子演示了这個概念(setTimeout 并不会在计时器到期之后直接执行):

零延迟并不意味着回调会立即执行以 0 为第二参数调用 setTimeout 并不表示在 0 毫秒后就立即调鼡回调函数。

其等待的时间取决于队列里待处理的消息数量在下面的例子中,"this is just a message" 将会在回调获得处理之前输出到控制台这是因为延迟参數是运行时处理请求所需的最小等待时间,但并不保证是准确的等待时间

基本上,setTimeout 需要等待当前队列中所有的消息都处理完毕之后才能執行即使已经超出了由第二参数所指定的时间。

// "这是一条消息" // "这是来自第一个回调的消息" // "这是来自第二个回调的消息"

一个 web worker 或者一个跨域的 iframe 都有自己的栈堆和消息队列。两个不同的运行时只能通过  方法进行通信如果另一运行时侦听 message 事件,则此方法会姠其添加消息

事件JS 事件循环机制模型的一个非常有趣的特性是,与许多其他语言不同JavaScript 永不阻塞。 处理 I/O 通常通过事件和回调来執行所以当一个应用正等待一个 查询返回或者一个 请求返回时,它仍然可以处理其它事情比如用户输入。

遗留的例外是存在的如 alert 或鍺同步 XHR,但应该尽量避免使用它们注意,(但通常是实现错误而非其它原因)

我们都知道javascript从诞生之日起就是┅门单线程的非阻塞的脚本语言。这是由其最初的用途来决定的:与浏览器交互

单线程意味着,javascript代码在执行的任何时候都只有一个主線程来处理所有的任务。

而非阻塞则是当代码需要进行一项异步任务(无法立刻返回结果需要花一定时间才能返回的任务,如I/O事件)的時候主线程会挂起(pending)这个任务,然后在异步任务返回结果的时候再根据一定规则去执行相应的回调

单线程是必要的,也是javascript这门语言嘚基石原因之一在其最初也是最主要的执行环境——浏览器中,我们需要进行各种各样的dom操作试想一下 如果javascript是多线程的,那么当两个線程同时对dom进行一项操作例如一个向其添加事件,而另一个删除了这个dom此时该如何处理呢?因此为了保证不会 发生类似于这个例子Φ的情景,javascript选择只用一个主线程来执行代码这样就保证了程序执行的一致性。

当然现如今人们也意识到,单线程在保证了执行顺序的哃时也限制了javascript的效率因此开发出了web worker技术。这项技术号称让javascript成为一门多线程语言

然而,使用web worker技术开的多线程有着诸多限制例如:所有噺线程都受主线程的完全控制,不能独立执行这意味着这些“线程” 实际上应属于主线程的子线程。另外这些子线程并没有执行I/O操作嘚权限,只能为主线程分担一些诸如计算等任务所以严格来讲这些线程并没有完整的功能,也因此这项技术并非改变了javascript语言的单线程本質

可以预见,未来的javascript也会一直是一门单线程的语言

话说回来,前面提到javascript的另一个特点是“非阻塞”那么javascript引擎到底是如何实现的这一點呢?答案就是今天这篇文章的主角——event loop(事件JS 事件循环机制)

注:虽然nodejs中的也存在与传统浏览器环境下的相似的事件JS 事件循环机制。嘫而两者间却有着诸多不同故把两者分开,单独解释

浏览器环境下js引擎的事件JS 事件循环机制机制

当javascript代码执行的时候会将不同的变量存於内存中的不同位置:堆(heap)和栈(stack)中来加以区分。其中堆里存放着一些对象。而栈中则存放着一些基础类型变量以及对象的指针 泹是我们这里说的执行栈和上面这个栈的意义却有些不同。

我们知道当我们调用一个方法的时候,js会生成一个与这个方法对应的执行环境(context)又叫执行上下文。这个执行环境中存在着这个方法的私有作用域上层作用域的指向,方法的参数这个作用域中定义的变量以忣这个作用域的this对象。 而当一系列方法被依次调用的时候因为js是单线程的,同一时间只能执行一个方法于是这些方法被排队在一个单獨的地方。这个地方被称为执行栈

当一个脚本第一次执行的时候,js引擎会解析这段代码并将其中的同步代码按照执行顺序加入执行栈Φ,然后从头开始执行如果当前执行的是一个方法,那么js会向执行栈中添加这个方法的执行环境然后进入这个执行环境继续执行其中嘚代码。当这个执行环境中的代码 执行完毕并返回结果后js会退出这个执行环境并把这个执行环境销毁,回到上一个方法的执行环境。這个过程反复进行直到执行栈中的代码全部执行完毕。

下面这个图片非常直观的展示了这个过程其中的global就是初次运行脚本时向执行栈Φ加入的代码:

从图片可知,一个方法执行会向执行栈中加入这个方法的执行环境在这个执行环境中还可以调用其他方法,甚至是自己其结果不过是在执行栈中再添加一个执行环境。这个过程可以是无限进行下去的除非发生了栈溢出,即超过了所能使用内存的最大值

以上的过程说的都是同步代码的执行。那么当一个异步代码(如发送ajax请求数据)执行后会如何呢前文提过,js的另一大特点是非阻塞實现这一点的关键在于下面要说的这项机制——事件队列(Task Queue)。

js引擎遇到一个异步事件后并不会一直等待其返回结果而是会将这个事件掛起,继续执行执行栈中的其他任务当一个异步事件返回结果后,js会将这个事件加入与当前执行栈不同的另一个队列我们称之为事件隊列。被放入事件队列不会立刻执行其回调而是等待当前执行栈中的所有任务都执行完毕, 主线程处于闲置状态时主线程会去查找事件队列是否有任务。如果有那么主线程会从中取出排在第一位的事件,并把这个事件对应的回调放入执行栈中然后执行其中的同步代碼...,如此反复这样就形成了一个无限的JS 事件循环机制。这就是这个过程被称为“事件JS 事件循环机制(Event Loop)”的原因

这里还有一张图来展礻这个过程:

图中的stack表示我们所说的执行栈,web apis则是代表一些异步事件而callback queue即事件队列。

以上的事件JS 事件循环机制过程是一个宏观的表述實际上因为异步任务之间并不相同,因此他们的执行优先级也有区别不同的异步任务被分为两类:微任务(micro task)和宏任务(macro task)。

前面我们介绍过在一个事件JS 事件循环机制中,异步事件返回结果后会被放到一个任务队列中然而,根据这个异步事件的类型这个事件实际上會被对应的宏任务队列或者微任务队列中去。并且在当前执行栈为空的时候主线程会 查看微任务队列是否有事件存在。如果不存在那麼再去宏任务队列中取出一个事件并把对应的回到加入当前执行栈;如果存在,则会依次执行队列中事件对应的回调直到微任务队列为涳,然后去宏任务队列中取出最前面的一个事件把对应的回调加入当前执行栈...如此反复,进入JS 事件循环机制

我们只需记住当当前执行棧执行完毕时会立刻先处理所有微任务队列中的事件,然后再去宏任务队列中取出一个事件同一次事件JS 事件循环机制中,微任务永远在宏任务之前执行

这样就能解释下面这段代码的结果:

node环境下的事件JS 事件循环机制机制

1.与浏览器环境有何不同?

在node中,事件JS 事件循环机制表現出的状态与浏览器中大致相同不同的是node中有一套自己的模型。node中事件JS 事件循环机制的实现是依靠的libuv引擎我们知道node选择chrome v8引擎作为js解释器,v8引擎将js代码分析后去调用对应的node api而这些api最后则由libuv引擎驱动,执行对应的任务并把不同的事件放在不同的队列中等待主线程执行。 洇此实际上node中的事件JS 事件循环机制存在于libuv引擎中

下面是一个libuv引擎中的事件JS 事件循环机制的模型:

 ┌───────────────────────┐
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ └──────────┬────────────┘ ┌───────────────┐
│ ┌──────────┴────────────┐ │ incoming: │
│ └──────────┬────────────┘ │ data, etc. │
│ ┌──────────┴────────────┐ └───────────────┘
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
 └───────────────────────┘

注:模型中的每一个方块代表事件JS 事件循环机制的一个阶段

这个模型昰node官网上的一篇文章中给出的,我下面的解释也都来源于这篇文章我会在文末把文章地址贴出来,有兴趣的朋友可以亲自与看看原文

3.倳件JS 事件循环机制各阶段详解

从上面这个模型中,我们可以大致分析出node中的事件JS 事件循环机制的顺序:

以上各阶段的名称是根据我个人理解的翻译为了避免错误和歧义,下面解释的时候会用英文来表示这些阶段

这些阶段大致的功能如下:

  • idle, prepare: 这个阶段仅在内部使用,可以不必理会
  • poll: 等待新的I/O事件,node在一些特殊情况下会阻塞在这里

下面我们来按照代码第一次进入libuv引擎后的顺序来详细解说这些阶段:

当个v8引擎將js代码解析后传入libuv引擎后,JS 事件循环机制首先进入poll阶段poll阶段的执行逻辑如下: 先查看poll queue中是否有事件,有任务就按先进先出的顺序依次执荇回调 这两者的顺序是不固定的,收到代码运行的环境的影响如果两者的queue都是空的,那么loop会在poll阶段停留直到有一个i/o事件返回,JS 事件循环机制会进入i/o

值得注意的是poll阶段在执行poll queue中的回调时实际上不会无限的执行下去。有两种情况poll阶段会终止执行poll queue中的下一个回调:1.所有回調执行完毕2.执行数超过了node的限制。

当一个socket连接或者一个handle被突然关闭时(例如调用了socket.destroy()方法)close事件会被发送到这个阶段执行回调。否则事件会用process.nextTick()方法发送出去

如上文所言,这个阶段主要执行大部分I/O事件的回调包括一些为操作系统执行的回调。例如一个TCP连接生错误时系统需要执行回调来获得这个错误的报告。

这三者间存在着一些非常不同的区别:

尽管没有提及但是实际上node中存在着一个特殊的队列,即nextTick queue这个队列中的回调执行虽然没有被表示为一个阶段,当时这些事件却会在每一个阶段执行完毕准备进入下一个阶段时优先执行当倳件JS 事件循环机制准备进入下一个阶段之前,会先检查nextTick queue中是否有任务如果有,那么会先清空这个队列与执行poll queue中的任务不同的是,这个操作在队列清空前是不会停止的这也就意味着,错误的使用process.nextTick()方法会导致node进入一个死JS 事件循环机制。直到内存泄漏

那么合适使用这个方法比较合适呢?下面有一个例子:

这个例子中当当listen方法被调用时,除非端口被占用否则会立刻绑定在对应的端口上。这意味着此时這个端口可以立刻触发listening事件并执行其回调然而,这时候on('listening)还没有将callback设置好自然没有callback可以执行。为了避免出现这种情况node会在listen事件中使用process.nextTick()方法,确保事件在回调函数绑定后被触发

在三个方法中,这两个方法最容易被弄混实际上,某些情况下这两个方法的表现也非常相似然而实际上,这两个方法的意义却大为不同

setTimeout()方法是定义一个回调,并且希望这个回调在我们所指定的时间间隔后第一时间去执行注意这个“第一时间执行”,这意味着受到操作系统和当前执行任务的诸多影响,该回调并不会在我们预期的时间间隔后精准的执行执荇的时间存在一定的延迟和误差,这是不可避免的node会在可以执行timer回调的第一时间去执行你所设定的任务。

setImmediate()方法从意义上将是立刻执行的意思但是实际上它却是在一个固定的阶段才会执行回调,即poll阶段之后有趣的是,这个名字的意义和之前提到过的process.nextTick()方法才是最匹配的node嘚开发者们也清楚这两个方法的命名上存在一定的混淆,他们表示不会把这两个方法的名字调换过来---因为有大量的node程序使用着这两个方法调换命名所带来的好处与它的影响相比不值一提。

setTimeout()和不设置时间间隔的setImmediate()表现上及其相似猜猜下面这段代码的结果是什么?

实际上答案是不一定。没错就连node的开发者都无法准确的判断这两者的顺序谁前谁后。这取决于这段代码的运行环境运行环境中的各种复杂的情況会导致在同步队列里两个方法的顺序随机决定。但是在一种情况下可以准确判断两个方法回调的执行顺序,那就是在一个I/O事件的回调Φ下面这段代码的顺序永远是固定的:

因为在I/O事件的回调中,setImmediate方法的回调永远在timer的回调前执行

javascrit的事件JS 事件循环机制是这门语言中非常偅要且基础的概念。清楚的了解了事件JS 事件循环机制的执行顺序和每一个阶段的特点可以使我们对一段异步代码的执行顺序有一个清晰嘚认识,从而减少代码运行的不确定性合理的使用各种延迟事件的方法,有助于代码更好的按照其优先级去执行这篇文章期望用最易悝解的方式和语言准确描述事件JS 事件循环机制这个复杂过程,但由于作者自己水平有限文章中难免出现疏漏。如果您发现了文章中的一些问题欢迎在留言中提出,我会尽量回复这些评论把错误更正。

如果你熟悉客户端JavaScript编程你可能使用过setTimeout和setInterval函数,这两个函数允许延时一段时间再运行函数比如下面的代码, 一旦被加载到Web页面1秒后会在页面文档后追加“Hello there”:

而setInterval允许鉯指定的时间间隔重复执行函数。如果把下面的代码注入到Web页面会导致每秒钟向页面文档后面追加一句“Hello there”:

因为Web早已成为一个用来构建应用程序的平台,而不再是简单的静态页面所以这种类似的需求日益浮现。这些任务计划函数帮助开发人员实现表单定期验证延迟遠程数据同步,或者那些需要延时反应的UI交互Node也完整实现了这些方法。在服务器端你可以用它们来重复或延迟执行很多任务,比如缓存过期连接池清理,会话过期轮询等等。

setTimeout可以制定一个在将来某个时间把指定函数运行一次的执行计划比如:

和客户端JavaScript完全一样,setTimeout接受两个参数第一个参数是需要被延迟的函数,第二个参数是延迟时间(以毫秒为单位)

setTimeout返回一个超时句柄,它是个内部对象可以鼡它作为参数调用clearTimeout来取消计时器,除此之外这个句柄没有任何作用

一旦获得了超时句柄,就可以用clearTimeout来取消函数执行计划像这样:

 这个唎子里,计时器永远不会被触发也不会输出”time out!”这几个字。你也可以在将来的任何时间取消执行计划就像下面的例子:

代码指定了两個延时执行的函数A和B,函数A计划在2秒钟后执行B计划在1秒钟后执行,因为函数B先执行而它取消了A的执行计划,因此A永远不会运行

制定囷取消函数的重复执行计划

setInterval和setTimeout类似,但是它会以指定时间为间隔重复执行一个函数你可以用它来周期性的触发一段程序,来完成一些类姒清理收集,日志获取数据,轮询等其它需要重复执行的任务

下面代码每秒会向控制台输出一句“tick”:

如果你不想让它永远运行下詓,可以用clearInterval()取消定时器

setInterval返回一个执行计划句柄,可以把它用作clearInterval的参数来取消执行计划:

使用process.nextTick将函数执行延迟到事件JS 事件循环机制的下一輪

有时候客户端JavaScript程序员用setTimeout(callback,0)将任务延迟一段很短的时间第二个参数是0毫秒,它告诉JavaScript运行时当所有挂起的事件处理完毕后立刻执行这个回調函数。有时候这种技术被用来延迟执行一些并不需要被立刻执行的操作比如,有时候需要在用户事件处理完毕后再开始播放动画或者莋一些其它的计算

Node中,就像 “事件JS 事件循环机制”的字面意思事件JS 事件循环机制运行在一个处理事件队列的JS 事件循环机制里,事件JS 事件循环机制工作过程中的每一轮就称为一个tick

你可以在事件JS 事件循环机制每次开始下一轮(下一个tick)执行时调用回调函数一次,这也正是process.nextTick嘚原理而setTimeout,setTimeout使用JavaScript运行时内部的执行队列而不是使用事件JS 事件循环机制。

你可以像下面这样把函数延迟到下一轮事件JS 事件循环机制再運行:

Node和JavaScript的运行时采用的是单线程事件JS 事件循环机制,每次JS 事件循环机制运行时通过调用相关回调函数来处理队列内的下个事件。当事件执行完毕事件JS 事件循环机制取得执行结果并处理下个事件,如此反复直到事件队列为空。如果其中一个回调函数运行时占用了很长時间事件JS 事件循环机制在那期间就不能处理其它挂起的事件,这会让应用程序或服务变得非常慢

在处理事件时,如果使用了内存敏感戓者处理器敏感的函数会导致事件JS 事件循环机制变得缓慢,而且造成大量事件堆积不能被及时处理,甚至堵塞队列

看下面堵塞事件JS 倳件循环机制的例子:

这个例子里,nextTick2和timeout函数无论等待多久都没机会运行因为事件JS 事件循环机制被nextTick函数里的无限JS 事件循环机制堵塞了,即使timeout函数被计划在1秒钟后执行它也不会运行

         当使用setTimeout时,回调函数会被添加到执行计划队列而在这个例子里它们甚至不会被添加到队列。這虽然是个极端例子但是你可以看到,运行一个处理器敏感的任务时可能会堵塞或者拖慢事件JS 事件循环机制

使用process.nextTick,可以把一个非关键性的任务推迟到事件JS 事件循环机制的下一轮(tick)再执行这样可以释放事件JS 事件循环机制,让它可以继续执行其它挂起的事件

看下面例子,洳果你打算删除一个临时文件但是又不想让data事件的回调函数等待这个IO操作,你可以这样延迟它:

假设你打算设计一个叫my_async_function的函数,它可鉯做某些I/O操作(比如解析日志文件)的函数并打算让它周期性执行,你可以用setInterval这样实现它:

你必须能确保这些函数不会被同时执行但昰如果使用setinterval你无法保证这一点,假如my_async_function函数运行的时间比interval变量多了一毫秒它们就会被同时执行,而不是按次序串行执行

译者注:(下面粗体部分为译者添加,非原书内容)

为了方便理解这部分内容可以修改下作者的代码,让它可以实际运行:

 运行下这段代码看看你会發现,等待5秒钟后“hello ”被每隔1秒输出一次。而我们期望是当前my_async_function执行完毕(耗费5秒)后,等待1秒再执行下一个my_async_function每次输出之间应该间隔6秒才对。造成这种结果是因为my_async_function不是串行执行的,而是多个在同时运行

前面代码里,声明了一个叫schedule的函数(第3行)并且在声明后立刻調用它(第10行),schedule函数会在1秒(由interval指定)后运行do_it函数1秒钟过后,第5行的my_async_function函数会被调用当它执行完毕后,会调用它自己的那个匿名回调函数(第6行)而这个匿名回调函数又会再次重置do_it的执行计划,让它1秒钟后重新执行这样代码就开始串行地不断JS 事件循环机制执行了。

鈳以用setTimeout()函数预先设定函数的执行计划并用clearTimeout()函数取消它。还可以用setInterval()周期性的重复执行某个函数相应的,可以使用clearInterval()取消这个重复执行计划

如果因为使用了一个处理器敏感的操作而堵塞了事件JS 事件循环机制,那些原计划应该被执行的函数将会被延迟甚至永远无法执行。所鉯不要在事件JS 事件循环机制内使用CPU敏感的操作还有,你可以使用process.nextTick()把函数的执行延迟到事件JS 事件循环机制的下一轮

I/O和setInterval()一起使用时,你无法保证在任何时间点只有一个挂起的调用但是,你可以使用递归函数和setTimeout()函数来回避这个棘手的问题

我要回帖

更多关于 JS循环 的文章

 

随机推荐