JavaScript并发模型

来源:互联网 发布:图像处理算法 编辑:程序博客网 时间:2024/06/08 16:47

本译文的原文 from Carbon Five:
本译文的主要目的是让你对事件驱动模型有一个基本了解,并且能够区分其与在Java、Python、Ruby等语言中使用的请求-应答模型。内容涉及到JavaScript并发模型的一些核心概念,主要包括事件轮询(event loop)和消息队列(message queue)。

非阻塞I/O

在JavaScript中,几乎所有的I/O都是非阻塞的,包括HTTP请求,数据库操作和硬盘读写。单个执行线程命令运行环境执行一个操作时通常提供一个回调函数,之后就继续处理后续的事件。当该操作完成时,相应的回调函数作为一个消息入队(enqueued)。在将来的某个时间点该消息会出队列(dequeued),相应的回调函数也被触发。
尽管交互模型对于习惯了UI的开发者很熟悉,但它与通常在服务器端应用中使用的同步、请求-应答模型不相同。
我们可以使用两段代码实现HTTP请求的过程来比较两者的不同。

# Rubyresponse = Faraday.get 'http://www.google.com'puts responseputs 'Done!'

执行过程很简单:

  • 执行get方法并且等待执行线程等待响应返回

  • 收到响应并且将其返回给调用者,并保存在变量中

  • 通过变量response输出响应消息

  • 输出消息“Done”

request('http://www.google.com', function(error, response, body) {  console.log(body);});console.log('Done!');
  • 执行请求函数,并传递一个异步函数作为将来某个时刻执行返回结果的回调

  • 立即执行console并输出“Done”

  • 收到响应,执行回调函数并打印响应体


事件循环

将调用者与响应解耦可以让JS运行时代码在等待异步操作完成和回调触发时做其它事情。但是这些回调存在于内存中的什么地方,以什么样的顺序执行并且什么条件下被执行?

JavaScript运行时代码包含了一个消息队列,专门用于存储需要被处理的消息以及相应的回调函数。这些消息以队列的方式排列并且用于响应给定了回调函数的外部事件(点击鼠标/HTTP请求等)。这意味着,如果你点击一个没有提供回调函数的按钮时,没有消息进入消息队列,浏览器也不会执行任何事件。

在一个循环汇中,队列轮询到下一个消息(每个轮询poll称作一个标号tick)。基本过程图下图:

图片来自原文

function init() {  var link = document.getElementById("foo");  link.addEventListener("click", function changeColor() {    this.style.color = "burlywood";  });}init();

上边的粟子,当用户点击foo元素并触发onclick事件时一个消息(包括事件以及对应的回调函数changeColor)进入队列。当该消息到达队列头并离开队列时,调用并执行回调函数changeColor(将其压入调用栈)。当changeColor返回或者抛出异常时事件循环就继续执行(调用栈为空,轮询到下一个消息)。

(简单讲:事件循环基本过程就是从消息队列的队列头取一个消息并将其回调函数压入调用栈;执行回调函数并返回结果;执行结束后弹出调用栈;当检测到调用栈为空后继续循环上述步骤。)


附加消息入队列

如果你的代码中调用的函数是一个异步函数(eg. setTimeout),提供的回调函数最终会在事件循环的未来某个轮次中作为不同队列消息的一部分执行。

举个粟子:

function f() {  console.log("foo");//输出“foo”  setTimeout(g, 0);//g进入消息队列,在下一轮event loop中执行并输出“bar”  console.log("baz");//输出“baz”  h();//输出“blix”}function g() {  console.log("bar");}function h() {  console.log("blix");}f();

由于setTimeout函数的非阻塞特性,它的回调函数会在至少0毫秒后被触发,并且不会被作为当前消息的一部分处理。调用setTimeout时,传入了两个参数:函数g和时间0毫秒;然后经过第二参数指定的时间后(本例中为0毫秒,立即),将单独的消息g入循环队列作为回调函数。因此最终的输出结果为:

这里写图片描述


Web Worker

使用Web Worker能够使你执行开销大的一些CPU密集型任务转移到一个单独的执行线程中,将主线程释放出来以执行其他操作。

Web Worker具有自己的消息队列、事件循环以及独立于实例化它的主线程的内存空间。主线程间与Web Worker间的通信是通过消息传递实现的,看起来和我们所看到的传统的事件触发的代码示例非常像。

Web Worker与主线程的通信

// js-event-loop-worker.js// our worker, which does some CPU-intensive operationvar reportResult = function(e) {  pi = SomeLib.computePiToSpecifiedDecimals(e.data);  postMessage(pi);//向主线程返回处理结果};onmessage = reportResult;//接收主线程的消息并处理
//js-event-loop-main-thread.js//our main code, in a <script>-tag in our HTML pagevar piWorker = new Worker("pi_calculator.js");//实例化一个workervar logResult = function(e) {//创建一个回调函数用于打印从worker返回的消息  console.log("PI: " + e.data);};piWorker.addEventListener("message", logResult, false);//为piWorker设置事件监听及回调函数piWorker.postMessage(100000);//向piWorker传递消息

在上边的例子中,主线程实例化了一个工作线程并为其“message”事件注册了logRegister回调函数。postMessage和onmessage用于传递和接收消息。当工作线程接收到来自主线程的消息,工作线程worker将其与reportResult回调加入自己的队列。离开队列时,消息传回到主线程中。通过这种方式,开发者可以将CPU密集型的操作委托给一个单独的线程,从而释放出主线程用于处理后续的消息和事件。


As you can see, javascript’s event-driven interaction mode is not rocket-science.

Good Luck.

原创粉丝点击