摘要:一異步編程原理顯然,上面這種方式和銀行取號等待有些類似,只不過銀行取號我們并不知道上一個人需要多久才會完成。下面來探討下中的異步編程原理。
眾所周知,JavaScript 的執行環境是單線程的,所謂的單線程就是一次只能完成一個任務,其任務的調度方式就是排隊,這就和火車站洗手間門口的等待一樣,前面的那個人沒有搞定,你就只能站在后面排隊等著。在事件隊列中加一個延時,這樣的問題便可以得到緩解。
A: 嘿,哥們兒,快點! B: 我要三分鐘,你先等著,完了叫你~ A: 好的,記得叫我啊~ 你(C)也等著吧,完了叫你~ C: 嗯!
告訴后面排隊的人一個準確的時間,這樣后面的人就可以利用這段時間去干點別的事情,而不是所有的人都排在隊列后抱怨。我寫了一段程序來解決這個問題:
/** * @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 基礎的童鞋都能夠看懂,利用上面這段代碼測試下:
// 進程記錄函數
function record(s){
var div = document.createElement("div");
div.innerHTML = s;
console.log(s);
document.body.appendChild(div);
}
Q
.q(function(){
record("0 3s 之后搞定,0 把 1 叫進來");
})
.q(3000) // 延時 3s
.q(function(){
record("1 2s 之后搞定,1 把 2 叫進來");
})
.q(2000) // 延時 2s
.q(function(){
record("2 后面沒人了,OK,廁所關門~");
})
.d(); // 執行隊列
可以戳戳這個 DEMO。
本文地址:http://barretlee.github.io/javascript-asynchronous-programming,轉載請注明出處。
一、Javascript 異步編程原理顯然,上面這種方式和銀行取號等待有些類似,只不過銀行取號我們并不知道上一個人需要多久才會完成。這是一種非阻塞的方式處理問題。下面來探討下 JavaScript 中的異步編程原理。
1. setTimeout 函數的弊端延時處理當然少不了 setTimeout 這個神器,很多人對 setTimeout 函數的理解就是:延時為 n 的話,函數會在 n 毫秒之后執行。事實上并非如此,這里存在三個問題,一個是 setTimeout 函數的及時性問題,可以測試下面這串代碼:
/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 和 函數作用域的轉換消耗了時間,其實不然,你可以再試試這段代碼:
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規范 中也是有提到的:
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 的時候,可以使用下面的方式替代,速度更快):
requestAnimationFrame 它允許 JavaScript 以 60+幀/s 的速度處理動畫,他的運行時間間隔比 setTimeout 是要短很多的。
process.nextTick 這個是 NodeJS 中的一個函數,利用他可以幾乎達到上面看到的 while 循環的效率
ajax 或者 插入節點 的 readState 變化
MutationObserver
setImmediate
...
這些東西下次有空再細談。之前研究司徒正美的 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 是異步編程不可缺少的角色,但是它本身就存在這么多的問題,這就要求我們用更加恰當的方式去規避!
2. 什么樣的函數為異步的異步的概念和非阻塞是是息息相關的,我們通過 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 的單線程特點,他沒有提供一種機制以阻止函數在其異步操作結束之前返回,事實上,除非函數返回,否則不會觸發任何異步事件。
3. 常見的異步模型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)。
默認狀態可以單向轉移到完成狀態,這個過程叫resolve,對應的方法是deferred.resolve(promiseOrValue);
默認狀態還可以單向轉移到失敗狀態,這個過程叫reject,對應的方法是deferred.reject(reason);
默認狀態時,還可以通過deferred.notify(update)來宣告任務執行信息,如執行進度;
狀態的轉移是一次性的,一旦任務由初始的pending轉為其他狀態,就會進入到下一個任務的執行過程中。
二、異步函數中的錯誤處理前面已經提到了 setTimeout 函數的一些問題,JS 中的 try..catch 機制并不能拿到 setTimeout 函數中出現的錯誤,一個 throw error 的影響范圍有多大呢?我做了一個測試:
從上面的測試我們可以看出,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 多線程技術介紹開始說了異步編程和非阻塞這個概念密切相關,而 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 做哪些事情呢?
數據的計算和加密 如計算斐波拉契函數的值,特別費時;再比如文件的 MD5 值比對,一個大文件的 MD5 值計算也是很費時的。
音、視頻流的編解碼工作,這些工作搞微信的技術人員應該沒有少做。有興趣的童鞋可以看看這個技術分享,是杭州的 hehe123 搞的一個WebRTC 分享,內容還不錯。
等等,你覺得費時間的事情都可以交給他做
然后要說的是 SharedWorker,這是 web 通信領域未來的一個趨勢,有些人覺得 WebSocket 已經十分不錯了,但是一些基于 WebSocket 的架構,服務器要為每一個頁面維護一個 WebSocket 代碼,而 SharedWorker 十分給力,他是多頁面通用的。
// 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通訊,尤其是游戲開發者,覺得是一個福音!
四、ECMAScript 6 中 Generator 對象搞定異步異步兩種常見方式是 事件監聽 以及 函數回調。前者沒什么好說的,事件機制是 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 中使用斷點一般,用起來特別舒服。
五、串并行的轉換留到下次說吧,文字敲多了,累 :)
六、小結本文提到了異步編程的相關概念和使用中會遇到的問題,在寫文章之前做了三天的調研,不過還是有很多點沒說全,下次對異步編程有了更深入的理解再來談一談。
七、參考資料Javascript異步編程的4種方法 阮一峰
javascript 異步編程 司徒正美
HTML Specification web develop group
Promise/A+ 規范
異步編程:When.js快速上手 JerrryQu
《Async Javascript》 By Trevor Burnham
非常有意義,卻尚未兼容的SharedWorker 次碳酸鈷
HTML5 Web Worker Franky
作者:Barret Lee
出處:http://barretlee.github.io/javascript-asynchronous-programming
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/85528.html
摘要:的翻譯文檔由的維護很多人說,阮老師已經有一本關于的書了入門,覺得看看這本書就足夠了。前端的異步解決方案之和異步編程模式在前端開發過程中,顯得越來越重要。為了讓編程更美好,我們就需要引入來降低異步編程的復雜性。 JavaScript Promise 迷你書(中文版) 超詳細介紹promise的gitbook,看完再不會promise...... 本書的目的是以目前還在制定中的ECMASc...
摘要:從最開始的到封裝后的都在試圖解決異步編程過程中的問題。為了讓編程更美好,我們就需要引入來降低異步編程的復雜性。寫一個符合規范并可配合使用的寫一個符合規范并可配合使用的理解的工作原理采用回調函數來處理異步編程。 JavaScript怎么使用循環代替(異步)遞歸 問題描述 在開發過程中,遇到一個需求:在系統初始化時通過http獲取一個第三方服務器端的列表,第三方服務器提供了一個接口,可通過...
摘要:如果我們假設文件和文件位于相同的目錄,那么代碼是這樣的其他方法如下編程步驟創建對象設置請求方式調用回調函數發送請求處理返回的結果創建對象一般來說手寫的時候,首先需要判斷該瀏覽器是否支持對象,如果支持則創建該對象,如果不支持則創建對象。 Ajax的簡介 什么是Ajax AJAX = Asynchronous JavaScript and XML(異步的 JavaScript 和 XML)...
摘要:函數會在之后的某個時刻觸發事件定時器。事件循環中的這樣一次遍歷被稱為一個。執行完畢并出棧。當定時器過期,宿主環境會把回調函數添加至事件循環隊列中,然后,在未來的某個取出并執行該事件。 原文請查閱這里,略有改動。 本系列持續更新中,Github 地址請查閱這里。 這是 JavaScript 工作原理的第四章。 現在,我們將會通過回顧單線程環境下編程的弊端及如何克服這些困難以創建令人驚嘆...
閱讀 2733·2023-04-25 14:15
閱讀 2686·2021-11-04 16:11
閱讀 3385·2021-10-14 09:42
閱讀 434·2019-08-30 15:52
閱讀 2820·2019-08-30 14:03
閱讀 3536·2019-08-30 13:00
閱讀 2105·2019-08-26 11:40
閱讀 3301·2019-08-26 10:25