伴隨著JavaScript這種web瀏覽器腳本語言的普及,對它的事件驅動交互模型,以及它與Ruby、Python和Java中常見的請求-響應模型的區別有一個基本了解,對您是有益的。在這篇文章中,我將解釋一些JavaScript並發模型的核心概念,包括其事件循環和訊息佇列,希望能夠提升你對一種語言的理解,這種語言你可能已經在使用但也許並不完全理解。
這篇文章是寫給誰的?
這篇文章是針對在客戶端或伺服器端使用或計劃使用JavaScript的web開發人員的。如果你已經精通事件循環,那麼這篇文章的大部分對你來說會很熟悉。對於那些還不是很精通的人,我希望能給你一個基本的了解,這樣可以更好地幫助你閱讀和編寫日常程式碼。
非阻塞I / O
在JavaScript中,幾乎所有的I/O都是非阻塞的。這包括HTTP請求,資料庫操作和磁碟讀寫,單執行緒執行要求在運行期執行一個操作時,提供一個回調函數,然後繼續做其它的事情。當操作已經完成時,訊息和已提供的回呼函數一起插入到佇列。在將來的某個時候,訊息從隊列移除,回調函數觸發。
雖然這種互動模型可能對已經習慣使用使用者介面的開發人員很熟悉,例如「mousedown,」和「click」事件在某一時刻被觸發。這與通常在伺服器端應用程式進行的同步式請求-回應模型是不同的。
讓我們來比較一下兩小塊程式碼,發出HTTP請求到www.google.com和輸出回應到控制台。首先來看看Ruby,搭配使用Faraday(一個Ruby 的HTTP 用戶端開發庫):
response = Faraday.get 'http://www.google.com' puts response puts 'Done!'
執行路徑很容易追蹤:
1、執行get方法,執行的執行緒等待,直到收到回應
2、從谷歌收到回應並回傳給呼叫者,它儲存在一個變數中
3.變數的值(在本例中,就是我們的回應)輸出到控制台
4、值“Done!“輸出到控制台
讓我們使用Node.js和Request函式庫在JavaScript做同樣的事情:
request('http://www.google.com', function(error, response, body) { console.log(body); }); console.log('Done!');
表面上看略有不同,實際行為截然不同:
1、執行請求函數,傳遞一個匿名函數作為回調,當回應在將來某個時候可用時執行回調。
2、“Done!“立即輸出到控制台
3.在將來的某個時候,響應返回和回調執行時,輸出它的內容到控制台
事件循環
將呼叫者和回應解耦,使得JavaScript在運行期等待非同步操作完成和回呼觸發時可以做其他事情。但是這些回調在記憶體中是如何組織的,以什麼順序執行?什麼導致他們被調用?
JavaScript執行時間包含一個訊息佇列,它儲存了需要處理的訊息的清單和相關的回呼函數。這些訊息是以佇列的形式來回應回呼函數所涉及的外部事件(如滑鼠點擊或收到HTTP請求的回應)的。例如,如果使用者點擊一個按鈕,但沒有提供回調函數,那麼也沒有訊息會加入佇列。
在一次循環,佇列提取下一則訊息(每次提取稱為一次「tick」),當事件發生,該訊息的回調執行。
回呼函數的呼叫在呼叫堆疊中作為初始化frame(片段),由於JavaScript是單執行緒的,未來的訊息提取和處理因為等待堆疊的所有呼叫返回而被停止。後續(同步)函數呼叫會新增新的呼叫frame到堆疊(例如,函數init呼叫函數changeColor)。
function init() { var link = document.getElementById("foo"); link.addEventListener("click", function changeColor() { this.style.color = "burlywood"; }); } init();
在這個範例中,當使用者點選「foo」元素時,一則訊息(及其回呼函數changeColor)會被插入到佇列,並觸發「onclick「事件。當訊息離開佇列時,其回調函數changeColor被呼叫。當changeColor回傳(或是拋出錯誤),事件循環仍在繼續。只要函數changeColor存在,並指定為「foo」元素的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 Workers
使用Web Workers允许您能够将一项费时的操作在一个单独的线程中执行,从而可以释放主线程去做别的事情。worker(工作线程)包括一个独立的消息队列,事件循 环,内存空间独立于实例化它的原始线程。worker和主线程之间的通信通过消息传递,看起来很像我们往常常见的传统事件代码示例。
首先,我们的worker:
// our worker, which does some CPU-intensive operation var reportResult = function(e) { pi = SomeLib.computePiToSpecifiedDecimals(e.data); postMessage(pi); }; onmessage = reportResult;
然后,主要的代码块在我们的HTML中以script-标签存在:
// 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函数注册到自己的“消息”事件中。当worker线程接收到主线程的消息,worker入队一条消息同时带上reportResult回调函数。消息出队时,一条新消息发送回主线程,新消息入队主线程队列(带上logResult回调函数)。这样,开发人员可以将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被调用,导致消息(带上changeHeader回调)被添加到消息队列,在大约100毫秒后执行。然后 changeHeaderDeferred函数返回false,结束第一个消息的处理,但header变量仍然可以通过闭包被引用,而不是被垃圾回收。当 第二个消息被处理(changeHeader函数),它保持了对在外部函数作用域中声明的header变量的访问。一旦第二个消息 (changeHeader函数)执行结束,header变量可以被垃圾回收。
提醒
JavaScript 事件驱动的交互模型不同于许多程序员习惯的请求-响应模型,但如你所见,它并不复杂。使用简单的消息队列和事件循环,JavaScript使得开发人员在构建他们的系统时使用大量asynchronously-fired(异步-触发)回调函数,让运行时环境能在等待外部事件触发的同时处理并发操作。然而,这不过是并发的一种方法。
以上就是本文的全部内容,希望对大家的学习有所帮助。