天天看點

JavaScript異步程式設計原理

衆所周知,JavaScript 的執行環境是單線程的,所謂的單線程就是一次隻能完成一個任務,其任務的排程方式就是排隊,這就和火車站洗手間門口的等待一樣,前面的那個人沒有搞定,你就隻能站在後面排隊等着。在事件隊列中加一個延時,這樣的問題便可以得到緩解。

告訴後面排隊的人一個準确的時間,這樣後面的人就可以利用這段時間去幹點别的事情,而不是所有的人都排在隊列後抱怨。我寫了一段程式來解決這個問題:

這段程式加了很多注釋,相信有 JS 基礎的童鞋都能夠看懂,利用上面這段代碼測試下:

也可以直接運作這段程式:

JavaScript異步程式設計原理

 //事件隊列管理,含延時運作代碼

顯然,上面這種方式和銀行取号等待有些類似,隻不過銀行取号我們并不知道上一個人需要多久才會完成。這是一種非阻塞的方式處理問題。下面來探讨下 JavaScript 中的異步程式設計原理。

延時處理當然少不了 setTimeout 這個神器,很多人對 setTimeout 函數的了解就是:延時為 n 的話,函數會在 n 毫秒之後執行。事實上并非如此,這裡存在三個問題,一個是 setTimeout 函數的及時性問題,可以測試下面這串代碼:

可以看出 1s 中運作的次數大概在 200次 左右,有人會說那是因為 new Date 和 函數作用域的轉換消耗了時間,其實不然,你可以再試試這段代碼:

為了讓函數可以更快速的相應,部分浏覽器提供了更加進階的接口(當 timeout 為 0 的時候,可以使用下面的方式替代,速度更快):

requestAnimationFrame 它允許 JavaScript 以 60+幀/s 的速度處理動畫,他的運作時間間隔比 setTimeout 是要短很多的。 @司徒正美,他适合動畫,使用他可以在 tab 失去焦點或者最小化的時候減緩運動,進而節省 CPU 資源,他的運作間隔确實比 setTimeout 要長。

process.nextTick 這個是 NodeJS 中的一個函數,利用他可以幾乎達到上面看到的 while 循環的效率

ajax 或者 插入節點 的 readyState 變化

MutationObserver 大約 2-3ms

setImmediate  

postMessage 這個相當快

...

上面說了一堆,目的是想說明, setTimeout 是存在一定時間間隔的,并不是設定 n 毫秒執行,他就是 n 毫秒執行,可能會有一點時間的延遲(2ms左右)。然後說說他的第二個缺點,先看代碼:

我們期望 console 在 1s 之後出結果,可事實上他卻是在 2075ms 之後運作的,這就是 JavaScript 單線程給我們帶來的煩惱,while循環阻塞了 setTimeout 函數的執行。接着是他的第三個毛病,try..catch捕捉不到他的錯誤:

可以說 setTimeout 是異步程式設計不可缺少的角色,但是它本身就存在這麼多的問題,這就要求我們用更加恰當的方式去規避!

異步的概念和非阻塞是是息息相關的,我們通過 ajax 請求資料的時候,一般采用的是異步的方式:

在 xhr.open 中我們把第三個參數設定為 true ,也就是異步加載,當 state 發生改變的時候,xhr 立即響應,觸發相關的函數。有人想過用這樣的方式來處理:

而事實上,這裡的判斷已經陷入了死循環,即便是 xhr 的 status 已經發生了改變,這個死循環也跳不出來,那麼這裡的異步是基于事件的。

某個函數會導緻将來再運作的另一個函數,後者取自于事件隊列(若後面這個函數是作為參數傳遞給前者的,則稱其為回調函數,簡稱為回調)。—— 摘自《Async Javascript》

由于 JavaScript 的單線程特點,他沒有提供一種機制以阻止函數在其異步操作結束之前傳回,事實上,除非函數傳回,否則不會觸發任何異步事件。

1) 最常見的一種方式是,高階函數(泛函數)

解耦程度特别低,如果送入的參數太多會顯得很亂!這是最常見的一種方式,把函數作為參數送入,然後回調。

2) 事件監聽

JS 和 浏覽器提供的原生方法基本都是基于事件觸發機制的,耦合度很低,不過事件不能得到流程控制。

3) 釋出/訂閱( Pub/Sub )

把事件全部交給 E 這個控制器管理,可以完全掌握事件被訂閱的次數,以及訂閱者的資訊,管理起來特别友善。

4) Promise 對象(deferred 對象)

在Promises/A規範中,每個任務都有三種狀态:預設(pending)、完成(fulfilled)、失敗(rejected)。

預設狀态可以單向轉移到完成狀态,這個過程叫resolve,對應的方法是deferred.resolve(promiseOrValue);

預設狀态還可以單向轉移到失敗狀态,這個過程叫reject,對應的方法是deferred.reject(reason);

預設狀态時,還可以通過deferred.notify(update)來宣告任務執行資訊,如執行進度;

狀态的轉移是一次性的,一旦任務由初始的pending轉為其他狀态,就會進入到下一個任務的執行過程中。

前面已經提到了 setTimeout 函數的一些問題,JS 中的 try..catch 機制并不能拿到 setTimeout 函數中出現的錯誤,一個 throw error 的影響範圍有多大呢?我做了一個測試:

從上面的測試我們可以看出,<code>throw new Error</code> 的作用範圍就是阻斷一個 script 标簽内的程式運作,但是不會影響下面的 script。這個測試沒什麼作用,隻是想告訴大家不要擔心一個 Error 會影響全局的函數執行。是以把代碼分為兩段,一段可能出錯的,一段確定不會出錯的,這樣不至于讓全局代碼都死掉,當然這樣的處理方式是不可取的。

慶幸的是 window 全局對象上有一個便利的函數,<code>window.error</code>,我們可以利用他捕捉到所有的錯誤,并作出相應的處理,比如:

我們可以對錯誤進行封裝處理:

很顯然,報錯已經不可怕了,利用 window 提供的 onerror 函數可以很友善地處理錯誤并作出及時的反應,如果出現了不可知的錯誤,可以把資訊 post 到背景,這也算是一個十分不錯的監控方式。

不過這樣的處理存在一個問題,所有的錯誤我們都給屏蔽了,但有些錯誤本應該阻斷所有程式的運作的。比如我們通過 ajax 擷取資料中出了錯誤,程式誤以為已經拿到了資料,本應該停下工作報出這個緻命的錯誤,但是這個錯誤被 window.onerror 給截獲了,進而進行了錯誤的處理。

window.onerror 算是一種特别暴力的容錯手段,try..catch 也是如此,他們底層的實作就是利用 C/C++ 中的 goto 語句實作,一旦發現錯誤,不管目前的堆棧有多深,不管代碼運作到了何處,直接跑到 頂層 或者 try..catch 捕獲的那一層,這種一腳踢開錯誤的處理方式并不是很好,我覺得。

開始說了異步程式設計和非阻塞這個概念密切相關,而 JavaScript 中的 Worker 對象可以建立一個獨立線程來處理資料,很自然的處理了阻塞問題。我們可以把繁重的計算任務交給 Worker 去倒騰,等他處理完了再把資料 Post 過來。

上面是一個簡單的例子,如果我們建立了多個 Worker,在監聽 onmessage 事件的時候還要判斷下 e.target 的值進而得知資料源,當然,我們也可以把資料源封裝在 e.message 中。

Worker 是一個有用的工具,我可以可以在 Worker 中使用 setTimeout,setInterval等函數,也可以拿到 navigator 的相關資訊,最重要的是他可以建立 ajax 對象和 WebSocket 對象,也就是說他可以直接向伺服器請求資料。不過他不能通路 DOM 的資訊,更不能直接處理 DOM,這個其實很好了解,主線程和 Worker 是兩個獨立的線程,如果兩者都可以修改 DOM,那豈不是得設定一個麻煩的互斥變量?!還有一個值得注意的點是,在 Worker 中我們可以使用 importScript 函數直接加載腳本,不過這個函數是同步的,也就是說他會當機 Worker 線程,直到 Script 加載完畢。

他可以添加多個參數,加載的順序就是 參數的順序。一般會使用 Worker 做哪些事情呢?

資料的計算和加密 如計算斐波拉契函數的值,特别費時;再比如檔案的 MD5 值比對,一個大檔案的 MD5 值計算也是很費時的。

等等,你覺得費時間的事情都可以交給他做

然後要說的是 SharedWorker,這是 web 通信領域未來的一個趨勢,有些人覺得 WebSocket 已經十分不錯了,但是一些基于 WebSocket 的架構,伺服器要為每一個頁面維護一個 WebSocket 代碼,而 SharedWorker 十分給力,他是多頁面通用的。

簡單了解 SharedWorker,就是把運作的一個線程作為 web背景程式,完全不需要背景腳本參與,這個對 web通訊,尤其是遊戲開發者,覺得是一個福音!

是不是有種想吐的感覺,一層一層的嵌套,雖說這種嵌套十分正常,倘若每段代碼都是這樣的呈現,相信二次開發者一定會累死!關于如何解耦我就不細說了,可以回頭看看上面那篇回調地獄的文章。

本文提到了異步程式設計的相關概念和使用中會遇到的問題,在寫文章之前做了三天的調研,不過還是有很多點沒說全,下次對異步程式設計有了更深入的了解再來談一談。

<a href="http://promisesaplus.com/" target="_blank">Promise/A+ 規範</a>

本文轉自Barret Lee部落格園部落格,原文連結:http://www.cnblogs.com/hustskyking/p/javascript-asynchronous-programming.html,如需轉載請自行聯系原作者

繼續閱讀