以前的文章提到了 JavaScript 中的異步編程,然而不管早就存在的 setTimeout
仍是 ES6 中的 Promise
,它們都是 阻塞
異步,執行函數的時候,會阻塞線程。setTimeout
只會把一個函數延後執行,但仍是在主線程中執行,執行函數的時候會阻塞線程。換句話說,setTimeout
只實現了過程間併發(concurrent)而未實現並行(parallel)。javascript
ES 規範並無定義多線程,Node.js 至今也沒有原生的多線程實現。然而在 HTML5 中卻定義了 Web Worker
用於實現瀏覽器中的多線程。java
引用 MDN 原文:git
Web Workers 使得一個Web應用程序能夠在與主執行線程分離的後臺線程中運行一個腳本操做。這樣作的好處是能夠在一個單獨的線程中執行費時的處理任務,從而容許主(一般是UI)線程運行而不被阻塞/放慢。
與樸素(原始)的多線程編程方式不一樣,Web Worker
一般不容許線程間共享數據,因此沒有線程同步、數據競爭等問題,更沒有沒有鎖(Mutex)和條件變量(Condition variable)等概念(注 1)。它們使用 postMessage 相互通訊,能夠認爲是 JS 中的參與者模式實現。各個 Worker 間數據獨立,不共享內存:postMessage 始終經過結構化克隆的方式深拷貝傳值。github
使用 Web Worker 也很是簡單,只須要預先在 Worker 中註冊 message
事件,在主線程中 postMessage
給 Worker 處理就行了。處理完後能夠再經過 postMessage
傳結果給主線程。編程
須要注意的是,Web Worker 中不能夠操做 DOM,一切與 DOM 操做相關的函數、類都不能使用(建立一個 DOM 元素髮回給主線程 appendChild 也不行),因此可使用的方法很是有限,只適用於處理數據(注 2)。segmentfault
前面提到 Promise 是阻塞異步,那是否能夠把要處理的數據轉發給某個 Worker 處理並返回一個 Promise,在處理完後將其 resolve 掉呢?數組
答案固然是能夠的,並且實現並不複雜。瀏覽器
首先固然是 new 一個 Worker 出來。須要注意的是 Worker 的構造函數 接受的是一個 JavaScript 腳本的 URL,能否接受 data-uri 看瀏覽器,實測 Chrome、Firefox 能夠,Safari、Edge 不行(會拋 SECURITY_ERR
異常)。多線程
簡單起見,這裏仍是採起 data-uri 的形式。考慮可移植性的話能夠先指定一個靜態文件,而後使用 postMessage 把函數體傳過去。併發
this._worker = new Worker('data:text/javascript,' + encodeURIComponent(`'use strict'; const __fn = ${fn}; onmessage = e => postMessage(__fn(...e.data));`));
Worker 中作了兩件事:
__fn
,其值 fn
是須要執行的函數。若是 fn 自己是一個函數對象,這裏將其轉換爲字符串,至關於把函數的源代碼拼到了字符串裏。__fn
,而後將 __fn
的返回值經過 postMessage 傳給主函數。function dispatch(...args) { return new Promise((resolve, reject) => { this._queue.push({ resolve, reject }); this._worker.postMessage(args); }); }
返回一個 Promise。注意這裏不能只是簡單的 postMessage。由於若是使用者屢次調用 dispatch 函數一次建立了多個 Promise,以後很難肯定是哪一個 Promise 完成了。這裏經過一個隊列記憶建立的 Promise 順序,而後依次 resolve(單個 Worker 處理 message 事件仍是順序執行的)。固然你也能夠多傳一個標記值給 Worker 用於標記被 resolve 的 Promise。
JavaScript 裏的隊列就是數組:
this._queue = [];
this._worker.onmessage = e => this._queue.shift().resolve(e.data); this._worker.onerror = e => this._queue.shift().reject(e.error);
onmessage 表示正常返回;onerror 表示出現了異常。對應的 Promise 的 resolve 和 reject 直接從隊列裏取出來。
class Dispatcher { constructor(fn) { this._queue = []; this._worker = new Worker('data:text/javascript,' + encodeURIComponent(`'use strict'; const __fn = ${fn}; onmessage = e => postMessage(__fn(...e.data));`)); this._worker.onmessage = e => this._queue.shift().resolve(e.data); this._worker.onerror = e => this._queue.shift().reject(e.error); } dispatch(...args) { return new Promise((resolve, reject) => { this._queue.push({ resolve, reject }); this._worker.postMessage(args); }); } }
這就是完整代碼了,總共不到 20 行。使用的話也很簡單:
const dispatcher = new Dispatcher(arr => { // 建立對象,把入口函數傳入 for (let i=0; i<1000; ++i) arr.sort(); // 耗費些時間 return arr; // 返回處理後的結果 }); const arr = Array.from({ length: 8192 }, () => Math.random() * 10000); // 須要處理的數據 dispatcher.dispatch(arr) // 派發給 Worker .then(res => console.log(res)); // 處理完畢後輸出
在瀏覽器中測試,會生成這樣一段代碼:
排序大數組 1000 次的同時 UI 響應仍然不受影響。
這裏還有一個線程池的版本,能夠建立多個 Worker 同時並行執行多個任務:https://github.com/CarterLi/T...
由於要區分到底是哪一個 Worker 完成運行,處理 Worker 返回值的邏輯複雜了一些,有什麼建議歡迎提出。
SharedArrayBuffer
後已經能夠在主線程和各 Web Worker 間共享數據,使用 Atomics.wait()
和 Atomics.wake()
還能夠實現傳統意義上的鎖和條件變量。但因爲其出現較晚且並不是使用 Web Worker 的主流方式,這裏不展開討論。