最近在搜索更详细的关于JS事件处理的资料。发现国内的大部分blog都是相互抄袭。而MDM对于这里的解释也并很少。翻阅了部分文章,发现这篇文章颇有价值。故译之。虽然文章写于2013年,可是依然具备很高参考价值javascript
原文:The JavaScript Event Loop: Explained java
对于目前Web
浏览器上最流行的脚本语言JavaScript
。这篇文章为你提供了该语言基本的事件驱动模型的讲解,它与那些典型的有求必应的语言好比Ruby
, Python
, Java
不一样。在这篇文章中,我会为你解释这些JavaScript
中并发模型的核心概念,包括事件循环,消息队列来帮助你提升对这门已经使用过,可是尚未完全理解的语言更深刻的理解。浏览器
这篇文章面向那些已经开始彷佛用JavaScript
语言从事Web
开发的工程师,或者是计划从事这项工做的人员。若是你已经很是熟悉JavaScript
的事件循环机制,那么你会以为这篇文章的内容对于你来讲已经再熟悉不过了。对于那些没有对事件循环充分了解的人,我但愿这篇文章可以帮助到你,这样才能让你更好的理解你天天面对的代码。ruby
再JavaScript
几乎全部的I/O都是非阻塞的。包括HTTP
请求,数据访问,读写磁盘。一个单线程在运行时去处理这些操做,提供一个回调函数,而后接着去作其它的事情。当操做完成了,这个回调函数提供的消息会被推送到队列中。在某个时间点,消息从队列中被移除,紧接着回调函数就被触发了。服务器
虽然这个交互模型对于不少开发者来讲已经很是熟悉了 -- 好比 mousedown
和 click
事件的处理,- 可是这与那种典型服务器端同步的请求处理不一样。闭包
让咱们对比一下向 www.google.com
发出请求后将返回的代码输出到控制台。首先,Ruby
的话:并发
response = Faraday.get 'http://www.google.com'
puts response
puts 'Done!'
复制代码
执行路径大概是这个样子的:异步
get
方法被执行,而后执行线程开始等待,直到收到响应Google
收到响应而且返回到回调并存储到一个变量中Done
被输出到控制台让咱们利用 Node.js
中完成同样的事情看看:函数
request('http://www.google.com', function(error, response, body) {
console.log(body);
});
console.log('Done!');
复制代码
看起来一个显著的不一样和不一样的行为:oop
Done
被立刻输出到控制套将请求的响应以回调函数的方式处理,容许 JavaScript
在等待异步操做成功返回并执行回调函数以前能够作一些其余的事情。可是,在内存中怎么执行这些回调的呢?执行顺序是什么样的呢?什么致使他们被调用的呢?
JavaScript
的运行环境有一个用于存储消息和用于关联回调函数的消息队列。这些消息以事件被注册的顺序进行排列(好比鼠标点击事件或者是 HTTP
请求响应事件)。好比用户点击一个按钮,若是没有该事件的回调函数被注册,那么就没有消息加入队列。
在循环中,队列轮询下一个消息(每一次轮询被看成是一个 tick
),若是有消息,那么就执行消息对应的回调。
回调函数的调用做为调用堆栈的初始帧,因为JavaScript
是单线程的,消息轮询和处理会被中止,直到堆栈内的回调函数所有返回。后续函数调用(同步)向堆栈添加新的调用(例如初始化颜色)。
function init() {
var link = document.getElementById("foo");
link.addEventListener("click", function changeColor() {
this.style.color = "burlywood";
});
}
init();
复制代码
在这个例子中,当用户点击页面元素,而后一个onclick
事件被触发,一个消息被压入队列中。当消息被压入队列,他的回调函数changeColor
被执行。当changeColor
返回的时候(也多是抛出异常),事件循环就继续执行。只要changeColor
被指定为onclick
的回调函数,后面在该元素的点击会致使更多的消息(以及相关changeColor
回调的消息)被压入队列。
若是函数在你的代码中被异步调用(好比 setTimeout
),在以后的事件循环中,回调函数会以另外一个消息队列的一部分被执行。好比:
function f() {
console.log("foo");
setTimeout(g, 0);
console.log("baz");
h();
}
function g() {
console.log("bar");
}
function h() {
console.log("blix");
}
f();
复制代码
因为setTimeout
是非阻塞的,它会在0毫秒后被执行,而且并非做为此消息的一部分被处理。在这个例子中,setTimeout
被调用,传入一个回调函数 g
和 一个超时事件 0 毫秒。当时间到了之后,一个以g
为回调函数的消息将会被压入队列中。控制台会输出相似: foo
, baz
, blix
而后在下一次事件循环输出: bar
。若是在同一个调用帧中(译者注:就是一个函数内)执行了两次 setTimeout
,传入相同的值(译者注: 时间间隔)。他们会按照前后顺序执行。
Web Worker
容许你将昂贵的操做转入到独立到线程中执行,节约主要线程去作其它事情。worker
具备独立的消息队列,事件循环,和独立的内存空间。worker
与主线程经过消息来完成通信,这看起来有点像以前的事件处理那样。
首先,咱们的 worker
:
// our worker, which does some CPU-intensive operation
var reportResult = function(e) {
pi = SomeLib.computePiToSpecifiedDecimals(e.data);
postMessage(pi);
};
onmessage = reportResult;
复制代码
而后是咱们的js
代码:
// our main code, in a <script>-tag in our HTML page
var piWorker = new Worker("pi_calculator.js");
var logResult = function(e) {
console.log("PI: " + e.data);
};
piWorker.addEventListener("message", logResult, false);
piWorker.postMessage(100000);
复制代码
在这个例子中,主线程衍生并启动一个worker
,而后并将logResult
这个回调加入到事件循环。在worker
中,reportResult
被注册到本身的message
事件中。当worker
从主线程接收到消息,worker
就会返回一个消息,所以就会致使reportResult
被执行。
当进行压栈的时候,一个消息会被推送到主线程,并被压入消息堆栈(而后执行回调函数)。经过这个方式,开发人员能够将cpu密集型操做委托给独立的线程,释放主线程去继续处理消息和事件。
JavaScript
支持在回调函数中使用闭包,该回调在执行时维持对建立它们的环境的访问,即便回调执行完建立了新的调用堆栈。对于知道回调是以不一样的消息被执行的要比知道回调被建立更有趣。思考下面的代码:
function changeHeaderDeferred() {
var header = document.getElementById("header");
setTimeout(function changeHeader() {
header.style.color = "red";
return false;
}, 100);
return false;
}
changeHeaderDeferred();
复制代码
这个例子中,changeHeaderDeferred
执行的时候包含了header
变量。setTimeout
被执行,100毫秒后一个消息被添加到消息队列中。changeHeaderDeferred
函数返回了false,结束了此次处理 - 可是回调函数中依然保留着header
的引用,因此没有被垃圾回收。当第二个消息被处理的时候,函数体外(changeHeaderDeferred
)它依然保持这对header
的声明。第二次处理完后,header
才被垃圾回收处理。