衆所周知,JavaScript 的執行環境是單線程的,所謂的單線程就是一次只能完成一個任務,其任務的調度方式就是排隊,這就和火車站洗手間門口的等待同樣,前面的那我的沒有搞定,你就只能站在後面排隊等着。在事件隊列中加一個延時,這樣的問題即可以獲得緩解。javascript
A: 嘿,哥們兒,快點! B: 我要三分鐘,你先等着,完了叫你~ A: 好的,記得叫我啊~ 你(C)也等着吧,完了叫你~ C: 嗯!
告訴後面排隊的人一個準確的時間,這樣後面的人就能夠利用這段時間去幹點別的事情,而不是全部的人都排在隊列後抱怨。我寫了一段程序來解決這個問題:html
/** * @author Barret Lee * @email barret.china@gmail.com * @description 事件隊列管理,含延時 */ var Q = { // 保存隊列信息 a: [], // 添加到隊列 queue q: function(d){ // 添加到隊列若是不是函數或者數字則不處理 if(!/function|number/.test(typeof d)) return; Q.a.push(d); // 返回對自身的引用 return Q; }, // 執行隊列 dequeue d: function(){ var s = Q.a.shift(); // 若是已經到了隊列盡頭則返回 if(!s) return; // 若是是函數,直接執行,而後繼續 dequeue if(typeof s === "function") { s(), Q.d(); return; } // 若是是數字,該數字做爲延遲時間,延遲 dequeue setTimeout(function(){ Q.d(); }, s); } };
這段程序加了不少註釋,相信有 JS 基礎的童鞋都可以看懂,利用上面這段代碼測試下:java
// 進程記錄函數 function record(s){ var div = document.createElement("div"); div.innerHTML = s; console.log(s); document.body.appendChild(div); } Q .q(function(){ record("0 <i style='color:blue'>3s 以後搞定,0 把 1 叫進來</i>"); }) .q(3000) // 延時 3s .q(function(){ record("1 <i style='color:blue'>2s 以後搞定,1 把 2 叫進來</i>"); }) .q(2000) // 延時 2s .q(function(){ record("2 <span style='color:red'>後面沒人了,OK,廁所關門~</span>"); }) .d(); // 執行隊列
能夠戳戳這個 DEMO。node
本文地址:http://barretlee.github.io/javascript-asynchronous-programming,轉載請註明出處。git
顯然,上面這種方式和銀行取號等待有些相似,只不過銀行取號咱們並不知道上一我的須要多久纔會完成。這是一種非阻塞的方式處理問題。下面來探討下 JavaScript 中的異步編程原理。github
延時處理固然少不了 setTimeout 這個神器,不少人對 setTimeout 函數的理解就是:延時爲 n 的話,函數會在 n 毫秒以後執行。事實上並不是如此,這裏存在三個問題,一個是 setTimeout 函數的及時性問題,能夠測試下面這串代碼:web
/var d = new Date, count = 0, f, timer; timer = setInterval(f = function (){ if(new Date - d > 1000) clearInterval(timer), console.log(count); count++; }, 0);
能夠看出 1s 中運行的次數大概在 200次 左右,有人會說那是由於 new Date 和 函數做用域的轉換消耗了時間,其實否則,你能夠再試試這段代碼:ajax
var d = new Date, count = 0; while(true) { if(new Date - d > 1000) { console.log(count); break; } count++; }
我這裏顯示的是 351813,也就是說 count 累加了 35W+ 次,這說明了什麼呢?setInterval 和 setTimeout 函數運轉的最短週期是 5ms 左右,這個數值在 HTML規範 中也是有提到的:chrome
5. Let timeout be the second method argument, or zero if the argument was omitted. 若是 timeout 參數沒有寫,默認爲 0 7. If nesting level is greater than 5, and timeout is less than 4, then increase timeout to 4. 若是嵌套的層次大於 5 ,而且 timeout 設置的數值小於 4 則直接取 4.
爲了讓函數能夠更快速的相應,部分瀏覽器提供了更加高級的接口(當 timeout 爲 0 的時候,可使用下面的方式替代,速度更快):編程
這些東西下次有空再細談。以前研究司徒正美的 avalon 源碼的時候,看到了相關的內容,有興趣的能夠看看:
//視瀏覽器狀況採用最快的異步回調 var BrowserMutationObserver = window.MutationObserver || window.WebKitMutationObserver if (BrowserMutationObserver) { //chrome18+, safari6+, firefox14+,ie11+,opera15 avalon.nextTick = function(callback) { //2-3ms var input = DOC.createElement("input") var observer = new BrowserMutationObserver(function(mutations) { mutations.forEach(function() { callback() }) }) observer.observe(input, { attributes: true }) input.setAttribute("value", Math.random()) } } else if (window.VBArray) { //IE下這個一般只要1ms,並且沒有反作用,不會發現請求, //setImmediate若是隻執行一次,與setTimeout同樣要140ms上下 avalon.nextTick = function(callback) { var node = DOC.createElement("script") node.onreadystatechange = function() { callback() //在interactive階段就觸發 node.onreadystatechange = null root.removeChild(node) node = null } root.appendChild(node) } } else { avalon.nextTick = function(callback) { setTimeout(callback, 0) } }
上面說了一堆,目的是想說明, setTimeout 是存在必定時間間隔的,並非設定 n 毫秒執行,他就是 n 毫秒執行,可能會有一點時間的延遲(2ms左右)。而後說說他的第二個缺點,先看代碼:
var d = new Date; setTimeout(function(){ console.log("show me after 1s, but you konw:" + (new Date - d)); }, 1000); while(1) if(new Date - d > 2000) break;
咱們指望 console 在 1s 以後出結果,可事實上他倒是在 2075ms 以後運行的,這就是 JavaScript 單線程給咱們帶來的煩惱,while循環阻塞了 setTimeout 函數的執行。接着是他的第三個毛病,try..catch捕捉不到他的錯誤:
try{ setTimeout(function(){ throw new Error("我不但願這個錯誤出現!") }, 1000); } catch(e){ console.log(e.message); }
能夠說 setTimeout 是異步編程不可缺乏的角色,可是它自己就存在這麼多的問題,這就要求咱們用更加恰當的方式去規避!
異步的概念和非阻塞是是息息相關的,咱們經過 ajax 請求數據的時候,通常採用的是異步的方式:
var xhr = new XMLHttpRequest(); xhr.open('GET', '/', true); xhr.send(); xhr.onreadystatechange = function(){ console.log(xhr.status); }
在 xhr.open 中咱們把第三個參數設置爲 true ,也就是異步加載,當 state 發生改變的時候,xhr 當即響應,觸發相關的函數。有人想過用這樣的方式來處理:
while(1) { if(xhr.status === "complete") { // dosomething(); break; } }
而事實上,這裏的判斷已經陷入了死循環,即使是 xhr 的 status 已經發生了改變,這個死循環也跳不出來,那麼這裏的異步是基於事件的。
某個函數會致使未來再運行的另外一個函數,後者取自於事件隊列(若後面這個函數是做爲參數傳遞給前者的,則稱其爲回調函數,簡稱爲回調)。—— 摘自《Async Javascript》
因爲 JavaScript 的單線程特色,他沒有提供一種機制以阻止函數在其異步操做結束以前返回,事實上,除非函數返回,不然不會觸發任何異步事件。
1) 最多見的一種方式是,高階函數(泛函數)
step1(function(res1){ step2(function(res2){ step3(function(res3){ //... }); }); });
解耦程度特別低,若是送入的參數太多會顯得很亂!這是最多見的一種方式,把函數做爲參數送入,而後回調。
2) 事件監聽
f.on("evt", g); function f(){ setTimeout(function(){ f.trigger("evt"); }) }
JS 和 瀏覽器提供的原生方法基本都是基於事件觸發機制的,耦合度很低,不過事件不能獲得流程控制。
3) 發佈/訂閱( Pub/Sub )
E.subscribe("evt", g); function f(){ setTimeout(function () { // f的任務代碼 E.publish("evt"); }, 1000); }
把事件所有交給 E 這個控制器管理,能夠徹底掌握事件被訂閱的次數,以及訂閱者的信息,管理起來特別方便。
4) Promise 對象(deferred 對象)
關於這裏的內容能夠看看 屈屈 寫的文章,說的比較詳細。
Promise/A+ 規範是對 Promise/A 規範的補充和修改,他出現的目的是爲了統一異步編程中的接口,JS中的異步編程是十分廣泛的事情,也出現了不少的異步庫,若是不統一接口,對開發者來講也是一件十分痛苦的事情。
在Promises/A規範中,每一個任務都有三種狀態:默認(pending)、完成(fulfilled)、失敗(rejected)。
前面已經提到了 setTimeout 函數的一些問題,JS 中的 try..catch 機制並不能拿到 setTimeout 函數中出現的錯誤,一個 throw error 的影響範圍有多大呢?我作了一個測試:
<script type="text/javascript"> throw new Error("error"); console.log("show me"); // 並無打印出來 </script> <script type="text/javascript"> console.log("show me"); // 打印出來了 </script>
從上面的測試咱們能夠看出,throw new Error
的做用範圍就是阻斷一個 script 標籤內的程序運行,可是不會影響下面的 script。這個測試沒什麼做用,只是想告訴你們不要擔憂一個 Error 會影響全局的函數執行。因此把代碼分爲兩段,一段可能出錯的,一段確保不會出錯的,這樣不至於讓全局代碼都死掉,固然這樣的處理方式是不可取的。
慶幸的是 window 全局對象上有一個便利的函數,window.error
,咱們能夠利用他捕捉到全部的錯誤,並做出相應的處理,好比:
window.onerror = function(msg, url, line){ console.log(msg, url, line); // 必須返回 true,不然 Error 仍是會觸發阻塞程序 return true; } setTimeout(function(){ throw new Error("error"); // console: //Uncaught Error: error path/to/ie6bug.html 99 }, 50);
咱們能夠對錯誤進行封裝處理:
window.onerror = function(msg, url, line){ // 截斷 "Uncaught Error: error",獲取錯誤類型 var type = msg.slice(16); switch(type){ case "TooLarge": console.log("The number is too large"); case "TooSmall": console.log("The number is too Small"); case "TooUgly": console.log("That's Barret Lee~"); // 若是不是咱們預約義的錯誤類型,則反饋給後臺監控 default: $ && $.post && $.post({ "msg": msg, "url": url, "line": line }) } // 記得這裏要返回 true,不然錯誤阻斷程序。 return true; } setTimeout(function(){ if( something ) throw new Error("TooUgly"); // console: //That's Barret Lee~ }, 50);
很顯然,報錯已經不可怕了,利用 window 提供的 onerror 函數能夠很方便地處理錯誤並做出及時的反應,若是出現了不可知的錯誤,能夠把信息 post 到後臺,這也算是一個十分不錯的監控方式。
不過這樣的處理存在一個問題,全部的錯誤咱們都給屏蔽了,但有些錯誤本應該阻斷全部程序的運行的。好比咱們經過 ajax 獲取數據中出了錯誤,程序誤覺得已經拿到了數據,本應該停下工做報出這個致命的錯誤,可是這個錯誤被 window.onerror 給截獲了,從而進行了錯誤的處理。
window.onerror 算是一種特別暴力的容錯手段,try..catch 也是如此,他們底層的實現就是利用 C/C++ 中的 goto 語句實現,一旦發現錯誤,無論目前的堆棧有多深,無論代碼運行到了何處,直接跑到 頂層 或者 try..catch 捕獲的那一層,這種一腳踢開錯誤的處理方式並非很好,我以爲。
開始說了異步編程和非阻塞這個概念密切相關,而 JavaScript 中的 Worker 對象能夠建立一個獨立線程來處理數據,很天然的處理了阻塞問題。咱們能夠把繁重的計算任務交給 Worker 去倒騰,等他處理完了再把數據 Post 過來。
var worker = new Worker("./outer.js"); worker.addEventListener("message", function(e){ console.log(e.message); }); worker.postMessage("data one"); worker.postMessage("data two"); // outer.js self.addEventListener("message", function(e){ self.postMessage(e.message); });
上面是一個簡單的例子,若是咱們建立了多個 Worker,在監聽 onmessage 事件的時候還要判斷下 e.target 的值從而得知數據源,固然,咱們也能夠把數據源封裝在 e.message 中。
Worker 是一個有用的工具,我能夠能夠在 Worker 中使用 setTimeout,setInterval等函數,也能夠拿到 navigator 的相關信息,最重要的是他能夠建立 ajax 對象和 WebSocket 對象,也就是說他能夠直接向服務器請求數據。不過他不能訪問 DOM 的信息,更不能直接處理 DOM,這個其實很好理解,主線程和 Worker 是兩個獨立的線程,若是二者均可以修改 DOM,那豈不是得設置一個麻煩的互斥變量?!還有一個值得注意的點是,在 Worker 中咱們可使用 importScript 函數直接加載腳本,不過這個函數是同步的,也就是說他會凍結 Worker 線程,直到 Script 加載完畢。
importScript("a.js", "b.js", "c.js");
他能夠添加多個參數,加載的順序就是 參數的順序。通常會使用 Worker 作哪些事情呢?
而後要說的是 SharedWorker,這是 web 通訊領域將來的一個趨勢,有些人以爲 WebSocket 已經十分不錯了,可是一些基於 WebSocket 的架構,服務器要爲每個頁面維護一個 WebSocket 代碼,而 SharedWorker 十分給力,他是多頁面通用的。
<input id="inp" /><input type="button" id="btn" value="發送" /> <script type="text/javascript"> var sw = new SharedWorder("./outer.js"); // 綁定事件 sw.port.onmessage = function(e){ console.log(e.data); }; btn.onclick = function(){ sw.port.postMessage(inp.value); inp.value = ""; }; // 建立鏈接,開始監聽 sw.port.start(); </script> // outer.js var pool = []; onconnect = function(e) { // 把鏈接的頁面放入鏈接池 pool.push(e.ports[0]); // 收到信息當即廣播 e.ports[0].onmessage = function(e){ for(var i = 0;i < pool.length; i++) // 廣播信息 pool[i].postMessage(e.data); }; };
簡單理解 SharedWorker,就是把運行的一個線程做爲 web後臺程序,徹底不須要後臺腳本參與,這個對 web通信,尤爲是遊戲開發者,以爲是一個福音!
異步兩種常見方式是 事件監聽 以及 函數回調。前者沒什麼好說的,事件機制是 JS 的核心,而函數回調這塊,過於深刻的嵌套簡直就是一個地獄,能夠看看這篇文章,這是一篇介紹異步編程的文章,什麼叫作「回調地獄」,能夠看看下面的例子:
fs.readdir(source, function(err, files) { if (err) { console.log('Error finding files: ' + err) } else { files.forEach(function(filename, fileIndex) { console.log(filename) gm(source + filename).size(function(err, values) { if (err) { console.log('Error identifying file size: ' + err) } else { console.log(filename + ' : ' + values) aspect = (values.width / values.height) widths.forEach(function(width, widthIndex) { height = Math.round(width / aspect) console.log('resizing ' + filename + 'to ' + height + 'x' + height) this.resize(width, height).write(destination + 'w' + width + '_' + filename, function(err) { if (err) console.log('Error writing file: ' + err) }) }.bind(this)) } }) }) } })
是否是有種想吐的感受,一層一層的嵌套,雖然說這種嵌套十分正常,假若每段代碼都是這樣的呈現,相信二次開發者必定會累死!關於如何解耦我就不細說了,能夠回頭看看上面那篇回調地獄的文章。
ECMAScript 6中有一個 Generator 對象,過段時間會對 ES6 中的新知識進行一一的探討,這裏很少說了,有興趣的同窗能夠看看 H-Jin 寫的一篇文章使用 (Generator) 生成器解決 JavaScript 回調嵌套問題,使用 yield 關鍵詞和 Generator 把嵌套給「拉直」了,這種方式就像是 chrome 的 DevTool 中使用斷點通常,用起來特別舒服。
留到下次說吧,文字敲多了,累 :)
本文提到了異步編程的相關概念和使用中會遇到的問題,在寫文章以前作了三天的調研,不過仍是有不少點沒說全,下次對異步編程有了更深刻的理解再來談一談。
做者:Barret Lee
出處:http://barretlee.github.io/javascript-asynchronous-programming