这篇文章是对我的认为讲解 JavaScript 事件循环比较清楚的一篇英文文章的简单翻译,原文地址是http://altitudelabs.com/blog/...。javascript
若是你像我同样,喜欢JavaScript,是的,你确定也会认同,JavaScript这门语言并不完美,严肃的说,没有任何一门计算机语言是完美的。尽管JavaScript确实存在一些缺陷,但我喜欢编写web程序以及如何用JavaScript构建可以链接世界的应用。java
JavaScript这门语言水很深,他复杂的内部原理须要花费一段时间才可以真正的理解。其中的事件循环机制就不太好理解。颇有可能一个多年使用JavaScript进行程序开发的人未必真正理解 JavaScript 的事件循环究竟是怎么工做的。无论怎样,经过本篇博客,我但愿可以揭示什么是事件循环以及可以让你以为其实它真的没那么复杂。web
当咱们想到JavaScript时,咱们一般会在Web浏览器的上下文中考虑它 - 这是有道理的,由于咱们大多数状况下是在客户端中(浏览器)运行JavaScript。而后,咱们须要清楚的知道(由于这很重要),运行一个web应用,涉及到一系列的技术术语,如 JavaScript 引擎(像chrome V8) , 一系列的Web API(像DOM,BOM),还有事件循环和事件队列。chrome
当看到这么多术语,你可能会想,"个人天哪(食屎啦),看起来超级复杂。。。",你的想法有必定道理,可是你很快会看到,应用运行的基本原理其实并无那么复杂,虽然具体的底层实现超出了咱们的范围。浏览器
在咱们深刻到事件循环以前,咱们须要理解下JavaScript引擎是干什么的?数据结构
事实上,对于JavaScript引擎的实现有不少,可是目前为止最知名的就是谷歌的Chrome 的 V8 引擎(V8 引擎不只仅只限存在于浏览器,它也存在于服务端,用于解析服务端的JavaScript 代码,如NodeJS)。那么,JavaScript 引擎到底作了些什么呢? 其实很简单,就是逐行逐句的处理JavaScript代码,没错,一次只能处理一句,因此JavaScript是单线程的。这样带来的主要问题是若是你运行的JavaScript语句须要很长时间才能返回,则这个语句后面的全部代码都会被阻塞。咱们固然不但愿咱们写的代码会阻塞,特别是在浏览器端,能够想象一下,若是你在一个网站上点击一个按钮,而后代码就挂起了,你尝试去单击该网站页面上的其余按钮,可是并无任何响应,会是怎么一种体验。这里最可能的缘由是点击按钮触发的代码运行须要很长时间,使得后面的代码被阻塞,致使整个网站UI没法同时再响应用户的交互事件。异步
那么 JavaScript 引擎是如何知道或者怎么作到一次只执行一句JavaScript语句的呢? 答案是经过调用栈,能够将调用栈想象成升降梯,第一我的进入升降梯将会在最后退出升降梯,然而最后一个进入的将会第一个出来。(做者在这里的比喻彷佛不太好理解,可是你们确定都学过数据结构中的栈,其特色就是先进后出)。咱们看下下面的例子:函数
/* Within main.js */ var firstFunction = function () { console.log("I'm first!"); }; var secondFunction = function () { firstFunction(); console.log("I'm second!"); }; secondFunction(); /* Results: * => I'm first! * => I'm second! */
而后下面是调用栈中序列状况:oop
首先是Main.js 匿名主函数被调用:网站
secondFunction 方法被调用:
调用 secondFunction 后致使 firstFunction 被调用:
执行 firstFunction 在控制台中打印了 "I'm first!",执行完后 firstFunction 中没有更多的语句能够被执行了,因此 firstFunction 被移出了调用栈:
执行继续,到 secondFunction 中,"I’m second!" 输出到控制台,一样 secondFunction 中没有其余更多的代码要被执行了,因此也从调用栈中移出了。以此类推,最后调用栈会置空。
如今咱们了解了JavaScript 引擎中的调用栈是怎么工做的,咱们继续回到刚才说到代码阻塞那里,咱们知道咱们应该去避免它,可是应该怎么作呢?幸运的是 JavaScript 提供了一种机制,它经过异步函数,不要担忧,异步函数其实和其余函数没什么区别,惟一区别是异步函数并不会当即立刻执行,会在后面某个时间点被触发执行。若是你用过setTimeout函数,你已经对异步函数熟悉了。咱们来看下下面的例子:
/* Within main.js */ var firstFunction = function () { console.log("I'm first!"); }; var secondFunction = function () { setTimeout(firstFunction, 5000); console.log("I'm second!"); }; secondFunction(); /* Results: * => I'm second! * (And 5 seconds later) * => I'm first! */
一样咱们接下来看下调用栈中序列状况:
在 secondFunction 执行到被放入调用栈以后,setTimeout 函数被调用,一样也放入了调用栈。
在 setTimeout 函数执行以后,有个特别的地方,浏览器将 setTimeout 的回调函数(在上面例子中,firstFunction) 放在了一个能够称为事件表(Event Table)的地方。 为了便于理解,咱们能够将这个事件表想象成注册表:调用栈告诉事件表注册特定的函数,只有当特定的事件发生了,这个函数才能被执行(应该是放入事件队列)。而后当事件发生后,事件表就会简单的将函数移动到事件队列(Event Queue)中。此事件队列的美妙之处在于,它只是函数等待被调用和移动到调用栈的一个临时存放区域。
你可能会问,"既然这样,那么事件队列里的这些函数何时会被移动到调用栈中执行?" 其实JavaScript引擎遵循着很是简单的规则:底层会有程序时不时的检查下调用栈是否为空,无论何时一旦为空,那么该程序会检查事件队列里是否会有正在等待被执行的函数。若是有,队列中的第一个函数会被移动到调用栈中而后被执行。若是事件队列为空,这个监视程序将会一直保持运行,瞧! 我刚刚描述的就是臭名昭着的事件循环(Event Loop)!
如今回到刚才的例子,执行setTimeout 函数,将回调函数(例子中:firstFunction) 移动到事件记录表中,而且按照五秒的时间延时进行注册:
这是另外一个“啊哈!”的时刻 - 注意一旦回调函数被移动到事件表,没有任何东西(后面的代码)被阻塞!程序继续运行。
在幕后,事件表会时不时监视是否有事件发生从而触发将对应的函数移动到事件队列中等待被执行。在咱们例子中,secondFunction 和 main.js 都完成了执行,调用栈为空。
在某一时刻,回调函数放在事件表中的时间将超过5秒。当发生这种状况时,事件表将firstFunction移动到事件队列中。
在事件循环不断监视调用栈是否为空,如今确实是空的时候,调用fistFunction,建立一个新的调用栈来执行代码。
在执行完firstFunction以后,进入了一个新的状态,这个状态调用栈为空,事件记录表为空,事件队列也为空。监视程序一种保持运行,一旦事件队列中存在待执行的函数,就会重复前面的步骤,执行函数,这就是事件循环。
我第一个认可个人解释掩盖了JavaScript引擎,事件表,事件队列和事件循环底层的实际实现细节。 然而,对于咱们绝大多数人来讲,咱们只须要对JavaScript执行异步功能时发生的状况有一个坚实的基础理解就能够了。 而且,我但愿上面的解释可以对你理解事件循环有帮助,这将是咱们做为Web开发人员所必须要了解的。