摘要:一異步編程原理顯然,上面這種方式和銀行取號等待有些類似,只不過銀行取號我們并不知道上一個人需要多久才會完成。下面來探討下中的異步編程原理。
眾所周知,JavaScript 的執(zhí)行環(huán)境是單線程的,所謂的單線程就是一次只能完成一個任務(wù),其任務(wù)的調(diào)度方式就是排隊,這就和火車站洗手間門口的等待一樣,前面的那個人沒有搞定,你就只能站在后面排隊等著。在事件隊列中加一個延時,這樣的問題便可以得到緩解。
A: 嘿,哥們兒,快點! B: 我要三分鐘,你先等著,完了叫你~ A: 好的,記得叫我啊~ 你(C)也等著吧,完了叫你~ C: 嗯!
告訴后面排隊的人一個準(zhǔn)確的時間,這樣后面的人就可以利用這段時間去干點別的事情,而不是所有的人都排在隊列后抱怨。我寫了一段程序來解決這個問題:
/** * @author Barret Lee * @email [email protected] * @description 事件隊列管理,含延時 */ var Q = { // 保存隊列信息 a: [], // 添加到隊列 queue q: function(d){ // 添加到隊列如果不是函數(shù)或者數(shù)字則不處理 if(!/function|number/.test(typeof d)) return; Q.a.push(d); // 返回對自身的引用 return Q; }, // 執(zhí)行隊列 dequeue d: function(){ var s = Q.a.shift(); // 如果已經(jīng)到了隊列盡頭則返回 if(!s) return; // 如果是函數(shù),直接執(zhí)行,然后繼續(xù) dequeue if(typeof s === "function") { s(), Q.d(); return; } // 如果是數(shù)字,該數(shù)字作為延遲時間,延遲 dequeue setTimeout(function(){ Q.d(); }, s); } };
這段程序加了很多注釋,相信有 JS 基礎(chǔ)的童鞋都能夠看懂,利用上面這段代碼測試下:
// 進(jìn)程記錄函數(shù)
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 叫進(jìn)來");
})
.q(3000) // 延時 3s
.q(function(){
record("1 2s 之后搞定,1 把 2 叫進(jìn)來");
})
.q(2000) // 延時 2s
.q(function(){
record("2 后面沒人了,OK,廁所關(guān)門~");
})
.d(); // 執(zhí)行隊列
可以戳戳這個 DEMO。
本文地址:http://barretlee.github.io/javascript-asynchronous-programming,轉(zhuǎn)載請注明出處。
一、Javascript 異步編程原理顯然,上面這種方式和銀行取號等待有些類似,只不過銀行取號我們并不知道上一個人需要多久才會完成。這是一種非阻塞的方式處理問題。下面來探討下 JavaScript 中的異步編程原理。
1. setTimeout 函數(shù)的弊端延時處理當(dāng)然少不了 setTimeout 這個神器,很多人對 setTimeout 函數(shù)的理解就是:延時為 n 的話,函數(shù)會在 n 毫秒之后執(zhí)行。事實上并非如此,這里存在三個問題,一個是 setTimeout 函數(shù)的及時性問題,可以測試下面這串代碼:
/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 中運行的次數(shù)大概在 200次 左右,有人會說那是因為 new Date 和 函數(shù)作用域的轉(zhuǎn)換消耗了時間,其實不然,你可以再試試這段代碼:
var d = new Date, count = 0; while(true) { if(new Date - d > 1000) { console.log(count); break; } count++; }
我這里顯示的是 351813,也就是說 count 累加了 35W+ 次,這說明了什么呢?setInterval 和 setTimeout 函數(shù)運轉(zhuǎn)的最短周期是 5ms 左右,這個數(shù)值在 HTML規(guī)范 中也是有提到的:
5. Let timeout be the second method argument, or zero if the argument was omitted. 如果 timeout 參數(shù)沒有寫,默認(rèn)為 0 7. If nesting level is greater than 5, and timeout is less than 4, then increase timeout to 4. 如果嵌套的層次大于 5 ,并且 timeout 設(shè)置的數(shù)值小于 4 則直接取 4.
為了讓函數(shù)可以更快速的相應(yīng),部分瀏覽器提供了更加高級的接口(當(dāng) timeout 為 0 的時候,可以使用下面的方式替代,速度更快):
requestAnimationFrame 它允許 JavaScript 以 60+幀/s 的速度處理動畫,他的運行時間間隔比 setTimeout 是要短很多的。
process.nextTick 這個是 NodeJS 中的一個函數(shù),利用他可以幾乎達(dá)到上面看到的 while 循環(huán)的效率
ajax 或者 插入節(jié)點 的 readState 變化
MutationObserver
setImmediate
...
這些東西下次有空再細(xì)談。之前研究司徒正美的 avalon 源碼的時候,看到了相關(guān)的內(nèi)容,有興趣的可以看看:
//視瀏覽器情況采用最快的異步回調(diào) 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,而且沒有副作用,不會發(fā)現(xiàn)請求, //setImmediate如果只執(zhí)行一次,與setTimeout一樣要140ms上下 avalon.nextTick = function(callback) { var node = DOC.createElement("script") node.onreadystatechange = function() { callback() //在interactive階段就觸發(fā) node.onreadystatechange = null root.removeChild(node) node = null } root.appendChild(node) } } else { avalon.nextTick = function(callback) { setTimeout(callback, 0) } }
上面說了一堆,目的是想說明, setTimeout 是存在一定時間間隔的,并不是設(shè)定 n 毫秒執(zhí)行,他就是 n 毫秒執(zhí)行,可能會有一點時間的延遲(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 之后出結(jié)果,可事實上他卻是在 2075ms 之后運行的,這就是 JavaScript 單線程給我們帶來的煩惱,while循環(huán)阻塞了 setTimeout 函數(shù)的執(zhí)行。接著是他的第三個毛病,try..catch捕捉不到他的錯誤:
try{ setTimeout(function(){ throw new Error("我不希望這個錯誤出現(xiàn)!") }, 1000); } catch(e){ console.log(e.message); }
可以說 setTimeout 是異步編程不可缺少的角色,但是它本身就存在這么多的問題,這就要求我們用更加恰當(dāng)?shù)姆绞饺ヒ?guī)避!
2. 什么樣的函數(shù)為異步的異步的概念和非阻塞是是息息相關(guān)的,我們通過 ajax 請求數(shù)據(jù)的時候,一般采用的是異步的方式:
var xhr = new XMLHttpRequest(); xhr.open("GET", "/", true); xhr.send(); xhr.onreadystatechange = function(){ console.log(xhr.status); }
在 xhr.open 中我們把第三個參數(shù)設(shè)置為 true ,也就是異步加載,當(dāng) state 發(fā)生改變的時候,xhr 立即響應(yīng),觸發(fā)相關(guān)的函數(shù)。有人想過用這樣的方式來處理:
while(1) { if(xhr.status === "complete") { // dosomething(); break; } }
而事實上,這里的判斷已經(jīng)陷入了死循環(huán),即便是 xhr 的 status 已經(jīng)發(fā)生了改變,這個死循環(huán)也跳不出來,那么這里的異步是基于事件的。
某個函數(shù)會導(dǎo)致將來再運行的另一個函數(shù),后者取自于事件隊列(若后面這個函數(shù)是作為參數(shù)傳遞給前者的,則稱其為回調(diào)函數(shù),簡稱為回調(diào))。—— 摘自《Async Javascript》
由于 JavaScript 的單線程特點,他沒有提供一種機(jī)制以阻止函數(shù)在其異步操作結(jié)束之前返回,事實上,除非函數(shù)返回,否則不會觸發(fā)任何異步事件。
3. 常見的異步模型1) 最常見的一種方式是,高階函數(shù)(泛函數(shù))
step1(function(res1){ step2(function(res2){ step3(function(res3){ //... }); }); });
解耦程度特別低,如果送入的參數(shù)太多會顯得很亂!這是最常見的一種方式,把函數(shù)作為參數(shù)送入,然后回調(diào)。
2) 事件監(jiān)聽
f.on("evt", g); function f(){ setTimeout(function(){ f.trigger("evt"); }) }
JS 和 瀏覽器提供的原生方法基本都是基于事件觸發(fā)機(jī)制的,耦合度很低,不過事件不能得到流程控制。
3) 發(fā)布/訂閱( Pub/Sub )
E.subscribe("evt", g); function f(){ setTimeout(function () { // f的任務(wù)代碼 E.publish("evt"); }, 1000); }
把事件全部交給 E 這個控制器管理,可以完全掌握事件被訂閱的次數(shù),以及訂閱者的信息,管理起來特別方便。
4) Promise 對象(deferred 對象)
關(guān)于這里的內(nèi)容可以看看 屈屈 寫的文章,說的比較詳細(xì)。
Promise/A+ 規(guī)范是對 Promise/A 規(guī)范的補充和修改,他出現(xiàn)的目的是為了統(tǒng)一異步編程中的接口,JS中的異步編程是十分普遍的事情,也出現(xiàn)了很多的異步庫,如果不統(tǒng)一接口,對開發(fā)者來說也是一件十分痛苦的事情。
在Promises/A規(guī)范中,每個任務(wù)都有三種狀態(tài):默認(rèn)(pending)、完成(fulfilled)、失敗(rejected)。
默認(rèn)狀態(tài)可以單向轉(zhuǎn)移到完成狀態(tài),這個過程叫resolve,對應(yīng)的方法是deferred.resolve(promiseOrValue);
默認(rèn)狀態(tài)還可以單向轉(zhuǎn)移到失敗狀態(tài),這個過程叫reject,對應(yīng)的方法是deferred.reject(reason);
默認(rèn)狀態(tài)時,還可以通過deferred.notify(update)來宣告任務(wù)執(zhí)行信息,如執(zhí)行進(jìn)度;
狀態(tài)的轉(zhuǎn)移是一次性的,一旦任務(wù)由初始的pending轉(zhuǎn)為其他狀態(tài),就會進(jìn)入到下一個任務(wù)的執(zhí)行過程中。
二、異步函數(shù)中的錯誤處理前面已經(jīng)提到了 setTimeout 函數(shù)的一些問題,JS 中的 try..catch 機(jī)制并不能拿到 setTimeout 函數(shù)中出現(xiàn)的錯誤,一個 throw error 的影響范圍有多大呢?我做了一個測試:
從上面的測試我們可以看出,throw new Error 的作用范圍就是阻斷一個 script 標(biāo)簽內(nèi)的程序運行,但是不會影響下面的 script。這個測試沒什么作用,只是想告訴大家不要擔(dān)心一個 Error 會影響全局的函數(shù)執(zhí)行。所以把代碼分為兩段,一段可能出錯的,一段確保不會出錯的,這樣不至于讓全局代碼都死掉,當(dāng)然這樣的處理方式是不可取的。
慶幸的是 window 全局對象上有一個便利的函數(shù),window.error,我們可以利用他捕捉到所有的錯誤,并作出相應(yīng)的處理,比如:
window.onerror = function(msg, url, line){ console.log(msg, url, line); // 必須返回 true,否則 Error 還是會觸發(fā)阻塞程序 return true; } setTimeout(function(){ throw new Error("error"); // console: //Uncaught Error: error path/to/ie6bug.html 99 }, 50);
我們可以對錯誤進(jìn)行封裝處理:
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~"); // 如果不是我們預(yù)定義的錯誤類型,則反饋給后臺監(jiān)控 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);
很顯然,報錯已經(jīng)不可怕了,利用 window 提供的 onerror 函數(shù)可以很方便地處理錯誤并作出及時的反應(yīng),如果出現(xiàn)了不可知的錯誤,可以把信息 post 到后臺,這也算是一個十分不錯的監(jiān)控方式。
不過這樣的處理存在一個問題,所有的錯誤我們都給屏蔽了,但有些錯誤本應(yīng)該阻斷所有程序的運行的。比如我們通過 ajax 獲取數(shù)據(jù)中出了錯誤,程序誤以為已經(jīng)拿到了數(shù)據(jù),本應(yīng)該停下工作報出這個致命的錯誤,但是這個錯誤被 window.onerror 給截獲了,從而進(jìn)行了錯誤的處理。
window.onerror 算是一種特別暴力的容錯手段,try..catch 也是如此,他們底層的實現(xiàn)就是利用 C/C++ 中的 goto 語句實現(xiàn),一旦發(fā)現(xiàn)錯誤,不管目前的堆棧有多深,不管代碼運行到了何處,直接跑到 頂層 或者 try..catch 捕獲的那一層,這種一腳踢開錯誤的處理方式并不是很好,我覺得。
三、JavaScript 多線程技術(shù)介紹開始說了異步編程和非阻塞這個概念密切相關(guān),而 JavaScript 中的 Worker 對象可以創(chuàng)建一個獨立線程來處理數(shù)據(jù),很自然的處理了阻塞問題。我們可以把繁重的計算任務(wù)交給 Worker 去倒騰,等他處理完了再把數(shù)據(jù) 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); });
上面是一個簡單的例子,如果我們創(chuàng)建了多個 Worker,在監(jiān)聽 onmessage 事件的時候還要判斷下 e.target 的值從而得知數(shù)據(jù)源,當(dāng)然,我們也可以把數(shù)據(jù)源封裝在 e.message 中。
Worker 是一個有用的工具,我可以可以在 Worker 中使用 setTimeout,setInterval等函數(shù),也可以拿到 navigator 的相關(guān)信息,最重要的是他可以創(chuàng)建 ajax 對象和 WebSocket 對象,也就是說他可以直接向服務(wù)器請求數(shù)據(jù)。不過他不能訪問 DOM 的信息,更不能直接處理 DOM,這個其實很好理解,主線程和 Worker 是兩個獨立的線程,如果兩者都可以修改 DOM,那豈不是得設(shè)置一個麻煩的互斥變量?!還有一個值得注意的點是,在 Worker 中我們可以使用 importScript 函數(shù)直接加載腳本,不過這個函數(shù)是同步的,也就是說他會凍結(jié) Worker 線程,直到 Script 加載完畢。
importScript("a.js", "b.js", "c.js");
他可以添加多個參數(shù),加載的順序就是 參數(shù)的順序。一般會使用 Worker 做哪些事情呢?
數(shù)據(jù)的計算和加密 如計算斐波拉契函數(shù)的值,特別費時;再比如文件的 MD5 值比對,一個大文件的 MD5 值計算也是很費時的。
音、視頻流的編解碼工作,這些工作搞微信的技術(shù)人員應(yīng)該沒有少做。有興趣的童鞋可以看看這個技術(shù)分享,是杭州的 hehe123 搞的一個WebRTC 分享,內(nèi)容還不錯。
等等,你覺得費時間的事情都可以交給他做
然后要說的是 SharedWorker,這是 web 通信領(lǐng)域未來的一個趨勢,有些人覺得 WebSocket 已經(jīng)十分不錯了,但是一些基于 WebSocket 的架構(gòu),服務(wù)器要為每一個頁面維護(hù)一個 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通訊,尤其是游戲開發(fā)者,覺得是一個福音!
四、ECMAScript 6 中 Generator 對象搞定異步異步兩種常見方式是 事件監(jiān)聽 以及 函數(shù)回調(diào)。前者沒什么好說的,事件機(jī)制是 JS 的核心,而函數(shù)回調(diào)這塊,過于深入的嵌套簡直就是一個地獄,可以看看這篇文章,這是一篇介紹異步編程的文章,什么叫做“回調(diào)地獄”,可以看看下面的例子:
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)) } }) }) } })
是不是有種想吐的感覺,一層一層的嵌套,雖說這種嵌套十分正常,倘若每段代碼都是這樣的呈現(xiàn),相信二次開發(fā)者一定會累死!關(guān)于如何解耦我就不細(xì)說了,可以回頭看看上面那篇回調(diào)地獄的文章。
ECMAScript 6中有一個 Generator 對象,過段時間會對 ES6 中的新知識進(jìn)行一一的探討,這里不多說了,有興趣的同學(xué)可以看看 H-Jin 寫的一篇文章使用 (Generator) 生成器解決 JavaScript 回調(diào)嵌套問題,使用 yield 關(guān)鍵詞和 Generator 把嵌套給“拉直”了,這種方式就像是 chrome 的 DevTool 中使用斷點一般,用起來特別舒服。
五、串并行的轉(zhuǎn)換留到下次說吧,文字敲多了,累 :)
六、小結(jié)本文提到了異步編程的相關(guān)概念和使用中會遇到的問題,在寫文章之前做了三天的調(diào)研,不過還是有很多點沒說全,下次對異步編程有了更深入的理解再來談一談。
七、參考資料Javascript異步編程的4種方法 阮一峰
javascript 異步編程 司徒正美
HTML Specification web develop group
Promise/A+ 規(guī)范
異步編程:When.js快速上手 JerrryQu
《Async Javascript》 By Trevor Burnham
非常有意義,卻尚未兼容的SharedWorker 次碳酸鈷
HTML5 Web Worker Franky
作者:Barret Lee
出處:http://barretlee.github.io/javascript-asynchronous-programming
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/85528.html
摘要:的翻譯文檔由的維護(hù)很多人說,阮老師已經(jīng)有一本關(guān)于的書了入門,覺得看看這本書就足夠了。前端的異步解決方案之和異步編程模式在前端開發(fā)過程中,顯得越來越重要。為了讓編程更美好,我們就需要引入來降低異步編程的復(fù)雜性。 JavaScript Promise 迷你書(中文版) 超詳細(xì)介紹promise的gitbook,看完再不會promise...... 本書的目的是以目前還在制定中的ECMASc...
摘要:從最開始的到封裝后的都在試圖解決異步編程過程中的問題。為了讓編程更美好,我們就需要引入來降低異步編程的復(fù)雜性。寫一個符合規(guī)范并可配合使用的寫一個符合規(guī)范并可配合使用的理解的工作原理采用回調(diào)函數(shù)來處理異步編程。 JavaScript怎么使用循環(huán)代替(異步)遞歸 問題描述 在開發(fā)過程中,遇到一個需求:在系統(tǒng)初始化時通過http獲取一個第三方服務(wù)器端的列表,第三方服務(wù)器提供了一個接口,可通過...
摘要:如果我們假設(shè)文件和文件位于相同的目錄,那么代碼是這樣的其他方法如下編程步驟創(chuàng)建對象設(shè)置請求方式調(diào)用回調(diào)函數(shù)發(fā)送請求處理返回的結(jié)果創(chuàng)建對象一般來說手寫的時候,首先需要判斷該瀏覽器是否支持對象,如果支持則創(chuàng)建該對象,如果不支持則創(chuàng)建對象。 Ajax的簡介 什么是Ajax AJAX = Asynchronous JavaScript and XML(異步的 JavaScript 和 XML)...
摘要:函數(shù)會在之后的某個時刻觸發(fā)事件定時器。事件循環(huán)中的這樣一次遍歷被稱為一個。執(zhí)行完畢并出棧。當(dāng)定時器過期,宿主環(huán)境會把回調(diào)函數(shù)添加至事件循環(huán)隊列中,然后,在未來的某個取出并執(zhí)行該事件。 原文請查閱這里,略有改動。 本系列持續(xù)更新中,Github 地址請查閱這里。 這是 JavaScript 工作原理的第四章。 現(xiàn)在,我們將會通過回顧單線程環(huán)境下編程的弊端及如何克服這些困難以創(chuàng)建令人驚嘆...
閱讀 2765·2023-04-25 14:15
閱讀 2708·2021-11-04 16:11
閱讀 3399·2021-10-14 09:42
閱讀 448·2019-08-30 15:52
閱讀 2830·2019-08-30 14:03
閱讀 3550·2019-08-30 13:00
閱讀 2117·2019-08-26 11:40
閱讀 3312·2019-08-26 10:25