JavaScript多執行緒程式設計

騰訊IVWEB團隊發表於2018-10-21

瀏覽器端JavaScript是以單執行緒的方式執行的,也就是說JavaScript和UI渲染佔用同一個主執行緒,那就意味著,如果JavaScript進行高負載的資料處理,UI渲染就很有可能被阻斷,瀏覽器就會出現卡頓,降低了使用者體驗。

為此,JavaScript提供了非同步操作,比如定時器(setTimeout、setInterval)事件、Ajax請求、I/O回撥等。我們可以把高負載的任務使用非同步處理,它們將會被放入瀏覽器的事件任務佇列(event loop)中去,等到JavaScript執行時執行執行緒空閒時候,事件佇列才會按照先進先出的原則被一一執行。

JavaScript多執行緒程式設計

nodejs引以為榮的非同步處理

通過類似定時器,回撥函式等非同步程式設計方式在平常的工作中已經足夠,但是如果做複雜運算,這種方式的不足就逐漸體現出來,比如settimeout拿到的值並不正確,或者頁面有複雜運算的時候很容易觸發假死狀態,非同步程式碼會影響主執行緒的程式碼執行,非同步終究還是單執行緒,不能從根本上解決問題。

多執行緒(Web Worker)就應運而生,它是HTML5標準的一部分,這一規範定義了一套 API,允許一段JavaScript程式執行在主執行緒之外的另外一個執行緒中。將一些任務分配給後者執行。在主執行緒執行的同時,Worker(子)執行緒在後臺執行,兩者互不干擾。等到 Worker 執行緒完成計算任務,再把結果返回給主執行緒。這樣的好處是,一些計算密集型或高延遲的任務,被 Worker 執行緒負擔了,主執行緒(通常負責 UI 互動)就會很流暢,不會被阻塞或拖慢。

一、什麼是web worker

JavaScript多執行緒程式設計

worker是window物件的一個方法,可以通過以下方式來檢測你的瀏覽器是否支援worker

if (window.Worker) {…… your code ……}
複製程式碼

一個worker是使用一個建構函式(Worker())建立的一個物件,這個建構函式需要傳入一個的JavaScript檔案,這個檔案包含將在工作執行緒中執行的程式碼。類似於這樣:

let myWorker = new Worker('worker.js');
複製程式碼

worker通過postMessage() 方法和onmessage事件進行資料通訊。主執行緒和子執行緒是雙向的,都可以傳送和監聽事件。向一個worker傳送訊息需要這樣做(main.js):

myWorker.postMessage('hello, world'); // 傳送
worker.onmessage = function (event) { // 接收
	console.log('Received message ' + event.data);
	doSomething();
}
複製程式碼

postMessage所傳的資料都是拷貝傳遞(ArrayBuffer型別除外),資料子執行緒也是類似傳遞(worker.js)

addEventListener('message', function (e) {
	postMessage('You said: ' + e.data);
}, false);
複製程式碼

當子執行緒執行結束後,使用完畢,為了節省系統資源,可以手動關閉子執行緒。如果worker沒有監聽訊息,那麼當所有任務執行完畢(包括計數器)後,它就會自動關閉。

// 在主執行緒中關閉
worker.terminate();
// 在子執行緒裡執行緒
close();

// 監聽 error 事件
worker.addEventListener('error', function (e) {
  console.log('ERROR', e);
});
複製程式碼

web worker本身很簡單,但是它的限制特別多。

二、使用的問題

1、同源限制
分配給Worker 執行緒執行的指令碼檔案(worker.js),必須與主執行緒的指令碼檔案(main.js)同源。這裡的同源限制包括協議、域名和埠,不支援本地地址(file://)。這會帶來一個問題,我們經常使用CDN來儲存js檔案,主執行緒的worker.js的域名指的是html檔案所在的域,通過new Worker(url)載入的url屬於CDN的域,會帶來跨域的問題,實際開發中我們不會吧所有的程式碼都放在一個檔案中讓子執行緒載入,肯定會選擇模組化開發。通過工具或庫把程式碼合併到一個檔案中,然後把子執行緒的程式碼生成一個檔案url。
解決方法:
(1)將動態生成的指令碼轉換成Blob物件。
(2)然後給這個Blob物件建立一個URL。
(3)最後將這個建立好的URL作為地址傳給Worker的建構函式。

let script = 'console.log("hello world!");'
let workerBlob = new Blob([script], { type: "text/javascript" });
let url = URL.createObjectURL(workerBlob);
let worker = new Worker(url);
複製程式碼

2、訪問限制
Worker子執行緒所在的全域性物件,與主執行緒不在同一個上下文環境,無法讀取主執行緒所在網頁的 DOM 物件,也無法使用document、window、parent這些物件,global物件的指向有變更,window需要改寫成self,不能執行alert()方法和confirm()等方法,只能讀取部分navigator物件內的資料。另外chrome的console.log()倒是可以使用,也支援debugger斷點,增加除錯的便利性。
3、使用非同步
Worker子執行緒中可以使用XMLHttpRequest 物件發出 AJAX 請求,可以使用setTimeout() setInterval()方法,也可使用websocket進行持續連結。也可以通過importScripts(url)載入另外的指令碼檔案,但是仍然不能跨域。

三、應用場景

1、使用專用執行緒進行數學運算
Web Worke設計的初衷就是用來做計算耗時任務,大資料的處理,而這種計算放在worker中並不會中斷前臺使用者的操作,避免程式碼卡頓帶來不必要的使用者體驗。例如處理ajax返回的大批量資料,讀取使用者上傳檔案,計算MD5,更改canvas的點陣圖的過濾,分析視訊和聲頻檔案等。worker中除了缺失了DOM和BOM操作能力以外,還是擁有非常強大的js邏輯運算處理的能力的,相當於nodejs一個級別的的執行環境。

2、高頻的使用者互動
高頻的使用者互動適用於根據使用者的輸入習慣、歷史記錄以及快取等資訊來協助使用者完成輸入的糾錯、校正功能等類似場景,使用者頻繁輸入的響應處理同樣可以考慮放在web worker中執行。例如,我們可以 做一個像Word一樣的應用:當使用者打字時後臺在詞典中進行查詢,幫助使用者自動糾錯等等。

3、資料的預取
對於一些有大量資料的前後臺互動產品,可以新開一個執行緒專門用來進行資料的預取和緩衝資料,本地web資料庫的行寫入和更改,長時間持續的執行,不會被主執行緒上的活動(比如使用者點選按鈕、提交表單)打斷,也有利於隨時響應主執行緒的通訊。也可以配合XMLHttpRequest和websocket進行不斷開的通訊,實現守衛程式。

四、相容性

JavaScript多執行緒程式設計

總體來說,相容性還是不錯的, 移動端可以放心使用,桌面端要求不高的話,也可以使用。

五、小結

對於web worker這項新技術,無論在PC還是在移動web,騰訊新聞前端組進行了廣泛的使用,Web Worker 的實現為前端程式帶來了後臺計算的能力,可以實現主 UI 執行緒與複雜計運算執行緒的分離,從而極大減輕了因計算量大而造成 UI 阻塞而出現的介面渲染卡、掉幀的情況,並且更大程度地利用了終端硬體的效能。superWorker能解決掉事件繫結,同源策略的問題,它目前最大的問題在於不相容IE9,在相容性要求不是那麼嚴格的地方,儘可能的使用吧!


《IVWEB 技術週刊》 震撼上線了,關注公眾號:IVWEB社群,每週定時推送優質文章。

相關文章