JavaScript的執行環境是單線程的,單線程的好處是執行環境簡單,不用去考慮諸如資源同步,死鎖等多線程阻塞式編程等所須要面對的惱人的問題。但帶來的壞處是當一個任務執行時間較長時,後面的任務會等待很長時間。在瀏覽器端就會出現瀏覽器假死,鼠標沒法響應等狀況。因此在瀏覽器端,耗時很長的操做都應該異步執行,避免瀏覽器失去響應。所謂異步執行,不一樣於同步執行(程序的執行順序與任務的排列順序是一致的、同步的),每個任務有一個或多個回調函數(callback),前一個任務結束後,不是執行後一個任務,而是執行回調函數,後一個任務則是不等前一個任務結束就執行,因此程序的執行順序與任務的排列順序是不一致的、異步的。既然Javascript是單線程的,那它又如何可以異步的執行呢?javascript
JavaScript有一個基於事件循環的併發模式。這個模式與C語言和java有很大不一樣。html
棧
函數調用造成堆棧幀。java
function f(b){ var a = 12; return a+b+35; } function g(x){ var m = 4; return f(m*x); } g(21);
當調用函數g時,建立第一個包含g參數和局部變量的幀。當g函數調用f函數時,建立包含f參數和局部變量第二個堆棧幀並推到第一個堆棧幀的頂部。當f返回時,頂部的堆棧幀元素被彈出(只留下g調用)。當g函數返回時,堆棧爲空。jquery
堆
堆是一個大型的非結構化區域,對象被分配到堆中。ios
隊列
一個javascript運行環境包含一個信息隊列,這個隊列是一系列將被執行的信息列表。每個消息被關聯到一個函數上。當堆棧爲空時,從消息隊列中取出一個消息並進行處理。該處理包含調用相關的函數(以及所以產生一個初始化的堆棧幀)。當堆棧再次爲空時,消息處理結束。git
事件循環的名字源於它的實現,常常像下面這樣:github
while(queue.waitForMessage()){ queue.processNextMessage(); }
queue.waitForMessage
同步等待一個消息。web
運行到完成
每一個消息徹底處理以後,其它消息纔會被處理。這樣的好處就是當一個函數不能被提早,只能等其餘函數執行完畢(而且能夠修改數據的函數操做)。這不一樣於C,例如,若是一個函數在一個線程運行時,它能夠停在任何點運行在另外一個線程一些其餘的代碼。這種模式的缺點是,若是一個消息時間過長完成,Web應用程序沒法處理像點擊或滾動的用戶交互。該瀏覽器可緩解此與「腳本花費的時間太長運行」對話框。一個很好的作法,遵循的是使信息處理短,若是可能削減一個消息到幾條消息。數據庫
添加消息
在網頁瀏覽器中,事件能夠在任什麼時候候添加,一個事件發生並伴隨事件監聽綁定到事件上。若是沒有事件監聽,則事件丟失。就像點擊一個元素,元素上綁定點擊事件。調用setTimeout
時,當函數的第二個參數時間被傳遞進去,將添加一個消息到隊列中。若是在隊列中沒有其餘消息,該消息被當即處理;然而,若是有消息,則setTimeout的信息將必須等待其它消息以進行處理。因爲這個緣由,第二個參數是最小的時間,而不是一個保證時間。編程
幾個運行環境之間的通訊
一個web worker或跨域iframe都有本身的堆棧,堆,和消息隊列。兩個不一樣的運行環境只能經過postMessage的方法發送消息進行通訊。這種方法增長了一個消息到其餘運行時,若是後者監聽消息事件。
事件循環模型是javascript的一個頗有意思的屬性,不像其它語言,它從不阻塞。假定瀏覽器中有一個專門用於事件調度的實例(該實例能夠是一個線程,咱們能夠稱之爲事件分發線程event dispatch thread),該實例的工做就是一個不結束的循環,從事件隊列中取出事件,處理全部很事件關聯的回調函數(event handler)。注意回調函數是在Javascript的主線程中運行的,而非事件分發線程中,以保證事件處理不會發生阻塞。經過事件和回調的I/O操做是一個典型的表現,因此當應用等待索引型數據庫查詢返回或XHR請求返回時,它仍然能夠處理其餘事情好比用戶輸入。
回調是javascript的基礎,函數被做爲參數進行傳遞。像下面:
f1(); f2(); f3();
若是f1中執行了大量的耗時操做,並且f2須要在f1以後執行。則程序能夠改成回調的形式。以下:
function f1(callback){ setTimeout(function () { // f1的大量耗時任務代碼並的到三個結果i,l,you. console.log("this is function1"); var i = "i", l = "love", y = "you"; if (callback && typeof(callback) === "function") { callback(i,l,y); } }, 50); } function f2(a, b, c) { alert(a + " " + b + " " + c); console.log("this is function2"); } function f3(){console.log("this is function3");} f1(f2); f3();
運行結果:
this is function3
this is function1
i love you
this is function2
採用這種方式,咱們把同步操做變成了異步操做,f1不會堵塞程序運行,至關於先執行程序的主要邏輯,將耗時的操做推遲執行。
回調函數的優勢是簡單,輕量級(不須要額外的庫)。缺點是各個部分之間高度耦合(Coupling),流程會很混亂,並且每一個任務只能指定一個回調函數。某個操做須要通過多個非阻塞的IO操做,每個結果都是經過回調,產生意大利麪條式(spaghetti)的代碼。
operation1(function(err, result) { operation2(function(err, result) { operation3(function(err, result) { operation4(function(err, result) { operation5(function(err, result) { // do something useful }) }) }) }) })
另外一種思路是採用事件驅動模式。任務的執行不取決於代碼的順序,而取決於某個事件是否發生。
// plain, non-jQuery version of hooking up an event handler var clickity = document.getElementById("clickity"); clickity.addEventListener("click", function (e) { //console log, since it's like ALL real world scenarios, amirite? console.log("Alas, someone is pressing my buttons…"); }); // the obligatory jQuery version $("#clickity").on("click", function (e) { console.log("Alas, someone is pressing my buttons…"); });
也能夠自定義事件進行監聽,關於自定義事件,屬於另一部分的內容。這種方法的優勢是比較容易理解,能夠綁定多個事件,每一個事件能夠指定多個回調函數,並且能夠"去耦合"(Decoupling),有利於實現模塊化。缺點是整個程序都要變成事件驅動型,運行流程會變得很不清晰。
咱們假定,存在一個"信號中心",某個任務執行完成,就向信號中心"發佈"(publish)一個信號,其餘任務能夠向信號中心"訂閱"(subscribe)這個信號,從而知道何時本身能夠開始執行。這就叫作"發佈/訂閱模式"(publish-subscribe pattern),又稱"觀察者模式"(observer pattern)。
var pubsub = (function(){ var q = {} topics = {}, subUid = -1; //發佈消息 q.publish = function(topic, args) { if(!topics[topic]) {return;} var subs = topics[topic], len = subs.length; while(len--) { subs[len].func(topic, args); } return this; }; //訂閱事件 q.subscribe = function(topic, func) { topics[topic] = topics[topic] ? topics[topic] : []; var token = (++subUid).toString(); topics[topic].push({ token : token, func : func }); return token; }; return q; //取消訂閱就不寫了,遍歷topics,而後經過保存前面返回token,刪除指定元素 })(); //觸發的事件 var f2 = function(topics, data) { console.log("logging:" + topics + ":" + data); console.log("this is function2"); } function f1(){ setTimeout(function () { // f1的任務代碼 console.log("this is function1"); //發佈消息'done' pubsub .publish('done', 'hello world'); }, 1000); } pubsub.subscribe('done', f2); f1();
上面代碼的運行結果爲:
this is function1
logging:done:hello world
this is function2
觀察者模式的實現方法有不少種,也能夠直接借用第三方庫。這種方法的性質與"事件監聽"相似(觀察者模式和自定義事件很是類似),可是明顯優於後者。觀察者模式和事件監聽同樣具備良好的去耦性,而且有一個消息中心,經過對消息中心的處理,能夠良好地監控程序運行。
Promises的概念是由CommonJS小組的成員在 Promises/A規範 中提出來的。Promises被逐漸用做一種管理異步操做回調的方法,但出於它們的設計,它們遠比那個有用得多。Promise容許咱們以同步的方式寫代碼,同時給予咱們代碼的異步執行。
function f1(){ var def = $.Deferred(); setTimeout(function () { // f1的任務代碼 console.log("this is f1"); def.resolve(); }, 500); return def.promise(); } function f2(){ console.log("this is f2"); } f1().then(f2);
上面代碼的運行結果爲:
this is f1
this is f2
上面引用的是jquery對Promises/A的實現,jquery中還有一系列方法,具體可參考:Deferred Object.關於Promises,強烈建議讀一下You're Missing the Point of Promises.還有不少第三方庫實現了Promises,如:Q、Bluebird、 mmDeferred 等。Promise(中文:承諾)其實爲一個有限狀態機,共有三種狀態:pending(執行中)、fulfilled(執行成功)和rejected(執行失敗)。其中pending爲初始狀態,fulfilled和rejected爲結束狀態(結束狀態表示promise的生命週期已結束)。狀態轉換關係爲:pending->fulfilled,pending->rejected。隨着狀態的轉換將觸發各類事件(如執行成功事件、執行失敗事件等)。 下節具體講述狀態機實現js異步編程。
Promises的本質實際就是經過狀態機來實現的,把異步操做與對象的狀態改變掛鉤,當異步操做結束的時候,發生相應的狀態改變,由此再觸發其餘操做。這要比回調函數、事件監聽、發佈/訂閱等解決方案,在邏輯上更合理,更易於下降代碼的複雜度。關於Promises可參考:JS魔法堂:剖析源碼理解Promises/A規範 。
這是一個新的技術,成爲2015年的ECMAScript(ES6)標準的一部分。該技術的規範已經完成,但實施狀況在不一樣的瀏覽器不一樣,在瀏覽器中的支持狀況以下。
桌面端:
手機端:
var f1 = new Promise(function(resolve, reject) { setTimeout(function () { // f1的任務代碼 console.log("this is f1"); resolve("Success"); }, 500); }); function f2(val){ console.log(val + ":" + "this is f2"); } function f3(){ console.log("this is f3") } f1.then(f2); f3();
以上代碼在Chrome 版本43中的運行結果爲:
this is f3
this is f1
Success:this is f2
更多關於ES6的Promise對象的特性可參考MDN中的Promise.
Asynchronous JS: Callbacks, Listeners, Control Flow Libs and Promises
Five Patterns to Help You Tame Asynchronous JavaScript
Javascript異步編程的4種方法
探索Javascript異步編程