本文由雲+社區發表
JavaScript 語言採用的是單線程模型,也就是說,全部任務只能在一個線程上完成,一次只能作一件事。前面的任務沒作完,後面的任務只能等着。隨着電腦計算能力的加強,尤爲是多核 CPU 的出現,單線程帶來很大的不便,沒法充分發揮計算機的計算能力。html
Web Worker 的做用,就是爲 JavaScript 創造多線程環境,容許主線程建立 Worker 線程,將一些任務分配給後者運行。在主線程運行的同時,Worker 線程在後臺運行,二者互不干擾。等到 Worker 線程完成計算任務,再把結果返回給主線程。這樣的好處是,一些計算密集型或高延遲的任務,被 Worker 線程負擔了,主線程(一般負責 UI 交互)就會很流暢,不會被阻塞或拖慢。json
Worker 線程一旦新建成功,就會始終運行,不會被主線程上的活動(好比用戶點擊按鈕、提交表單)打斷。這樣有利於隨時響應主線程的通訊。可是,這也形成了 Worker 比較耗費資源,不該該過分使用,並且一旦使用完畢,就應該關閉。api
Web Worker 有如下幾個使用注意點。瀏覽器
(1)同源限制緩存
分配給 Worker 線程運行的腳本文件,必須與主線程的腳本文件同源。服務器
(2)DOM 限制網絡
Worker 線程所在的全局對象,與主線程不同,沒法讀取主線程所在網頁的 DOM 對象,也沒法使用document
、window
、parent
這些對象。可是,Worker 線程能夠navigator
對象和location
對象。多線程
(3)通訊聯繫app
Worker 線程和主線程不在同一個上下文環境,它們不能直接通訊,必須經過消息完成。ide
(4)腳本限制
Worker 線程不能執行alert()
方法和confirm()
方法,但可使用 XMLHttpRequest 對象發出 AJAX 請求。
(5)文件限制
Worker 線程沒法讀取本地文件,即不能打開本機的文件系統(file://
),它所加載的腳本,必須來自網絡。
主線程採用new
命令,調用Worker()
構造函數,新建一個 Worker 線程。
var worker = new Worker('work.js');
Worker()
構造函數的參數是一個腳本文件,該文件就是 Worker 線程所要執行的任務。因爲 Worker 不能讀取本地文件,因此這個腳本必須來自網絡。若是下載沒有成功(好比404錯誤),Worker 就會默默地失敗。
而後,主線程調用worker.postMessage()
方法,向 Worker 發消息。
worker.postMessage('Hello World'); worker.postMessage({method: 'echo', args: ['Work']});
worker.postMessage()
方法的參數,就是主線程傳給 Worker 的數據。它能夠是各類數據類型,包括二進制數據。
接着,主線程經過worker.onmessage
指定監聽函數,接收子線程發回來的消息。
worker.onmessage = function (event) { console.log('Received message ' + event.data); doSomething(); } function doSomething() { // 執行任務 worker.postMessage('Work done!'); }
上面代碼中,事件對象的data
屬性能夠獲取 Worker 發來的數據。
Worker 完成任務之後,主線程就能夠把它關掉。
worker.terminate();
Worker 線程內部須要有一個監聽函數,監聽message
事件。
self.addEventListener('message', function (e) { self.postMessage('You said: ' + e.data); }, false);
上面代碼中,self
表明子線程自身,即子線程的全局對象。所以,等同於下面兩種寫法。
// 寫法一 this.addEventListener('message', function (e) { this.postMessage('You said: ' + e.data); }, false); // 寫法二 addEventListener('message', function (e) { postMessage('You said: ' + e.data); }, false);
除了使用self.addEventListener()
指定監聽函數,也可使用self.onmessage
指定。監聽函數的參數是一個事件對象,它的data
屬性包含主線程發來的數據。self.postMessage()
方法用來向主線程發送消息。
根據主線程發來的數據,Worker 線程能夠調用不一樣的方法,下面是一個例子。
self.addEventListener('message', function (e) { var data = e.data; switch (data.cmd) { case 'start': self.postMessage('WORKER STARTED: ' + data.msg); break; case 'stop': self.postMessage('WORKER STOPPED: ' + data.msg); self.close(); // Terminates the worker. break; default: self.postMessage('Unknown command: ' + data.msg); }; }, false);
上面代碼中,self.close()
用於在 Worker 內部關閉自身。
Worker 內部若是要加載其餘腳本,有一個專門的方法importScripts()
。
importScripts('script1.js');
該方法能夠同時加載多個腳本。
importScripts('script1.js', 'script2.js');
主線程能夠監聽 Worker 是否發生錯誤。若是發生錯誤,Worker 會觸發主線程的error
事件。
worker.onerror(function (event) { console.log([ 'ERROR: Line ', e.lineno, ' in ', e.filename, ': ', e.message ].join('')); }); // 或者 worker.addEventListener('error', function (event) { // ... });
Worker 內部也能夠監聽error
事件。
使用完畢,爲了節省系統資源,必須關閉 Worker。
// 主線程 worker.terminate(); // Worker 線程 self.close();
前面說過,主線程與 Worker 之間的通訊內容,能夠是文本,也能夠是對象。須要注意的是,這種通訊是拷貝關係,便是傳值而不是傳址,Worker 對通訊內容的修改,不會影響到主線程。事實上,瀏覽器內部的運行機制是,先將通訊內容串行化,而後把串行化後的字符串發給 Worker,後者再將它還原。
主線程與 Worker 之間也能夠交換二進制數據,好比 File、Blob、ArrayBuffer 等類型,也能夠在線程之間發送。下面是一個例子。
// 主線程 var uInt8Array = new Uint8Array(new ArrayBuffer(10)); for (var i = 0; i < uInt8Array.length; ++i) { uInt8Array[i] = i * 2; // [0, 2, 4, 6, 8,...] } worker.postMessage(uInt8Array); // Worker 線程 self.onmessage = function (e) { var uInt8Array = e.data; postMessage('Inside worker.js: uInt8Array.toString() = ' + uInt8Array.toString()); postMessage('Inside worker.js: uInt8Array.byteLength = ' + uInt8Array.byteLength); };
可是,拷貝方式發送二進制數據,會形成性能問題。好比,主線程向 Worker 發送一個 500MB 文件,默認狀況下瀏覽器會生成一個原文件的拷貝。爲了解決這個問題,JavaScript 容許主線程把二進制數據直接轉移給子線程,可是一旦轉移,主線程就沒法再使用這些二進制數據了,這是爲了防止出現多個線程同時修改數據的麻煩局面。這種轉移數據的方法,叫作Transferable Objects。這使得主線程能夠快速把數據交給 Worker,對於影像處理、聲音處理、3D 運算等就很是方便了,不會產生性能負擔。
若是要直接轉移數據的控制權,就要使用下面的寫法。
// Transferable Objects 格式 worker.postMessage(arrayBuffer, [arrayBuffer]); // 例子 var ab = new ArrayBuffer(1); worker.postMessage(ab, [ab]);
一般狀況下,Worker 載入的是一個單獨的 JavaScript 腳本文件,可是也能夠載入與主線程在同一個網頁的代碼。
<!DOCTYPE html> <body> <script id="worker" type="app/worker"> addEventListener('message', function () { postMessage('some message'); }, false); </script> </body> </html>
上面是一段嵌入網頁的腳本,注意必須指定<script>
標籤的type
屬性是一個瀏覽器不認識的值,上例是app/worker
。
而後,讀取這一段嵌入頁面的腳本,用 Worker 來處理。
var blob = new Blob([document.querySelector('#worker').textContent]); var url = window.URL.createObjectURL(blob); var worker = new Worker(url); worker.onmessage = function (e) { // e.data === 'some message' };
上面代碼中,先將嵌入網頁的腳本代碼,轉成一個二進制對象,而後爲這個二進制對象生成 URL,再讓 Worker 加載這個 URL。這樣就作到了,主線程和 Worker 的代碼都在同一個網頁上面。
有時,瀏覽器須要輪詢服務器狀態,以便第一時間得知狀態改變。這個工做能夠放在 Worker 裏面。
function createWorker(f) { var blob = new Blob([f.toString()]); var url = window.URL.createObjectURL(blob); var worker = new Worker(url); return worker; } var pollingWorker = createWorker(function (e) { var cache; function compare(new, old) { ... }; setInterval(function () { fetch('/my-api-endpoint').then(function (res) { var data = res.json(); if (!compare(data, cache)) { cache = data; self.postMessage(data); } }) }, 1000) }); pollingWorker.onmessage = function () { // render data } pollingWorker.postMessage('init');
上面代碼中,Worker 每秒鐘輪詢一次數據,而後跟緩存作比較。若是不一致,就說明服務端有了新的變化,所以就要通知主線程。
Worker 線程內部還能再新建 Worker 線程。下面的例子是將一個計算密集的任務,分配到10個 Worker。
主線程代碼以下。
var worker = new Worker('worker.js'); worker.onmessage = function (event) { document.getElementById('result').textContent = event.data; };
Worker 線程代碼以下。
// worker.js // settings var num_workers = 10; var items_per_worker = 1000000; // start the workers var result = 0; var pending_workers = num_workers; for (var i = 0; i < num_workers; i += 1) { var worker = new Worker('core.js'); worker.postMessage(i items_per_worker); worker.postMessage((i + 1) items_per_worker); worker.onmessage = storeResult; } // handle the results function storeResult(event) { result += event.data; pending_workers -= 1; if (pending_workers <= 0) postMessage(result); // finished! }
上面代碼中,Worker 線程內部新建了10個 Worker 線程,而且依次向這10個 Worker 發送消息,告知了計算的起點和終點。計算任務腳本的代碼以下。
// core.js var start; onmessage = getStart; function getStart(event) { start = event.data; onmessage = getEnd; } var end; function getEnd(event) { end = event.data; onmessage = null; work(); } function work() { var result = 0; for (var i = start; i < end; i += 1) { // perform some complex calculation here result += 1; } postMessage(result); close(); }
瀏覽器原生提供Worker()
構造函數,用來供主線程生成 Worker 線程。
var myWorker = new Worker(jsUrl, options);
Worker()
構造函數,能夠接受兩個參數。第一個參數是腳本的網址(必須遵照同源政策),該參數是必需的,且只能加載 JS 腳本,不然會報錯。第二個參數是配置對象,該對象可選。它的一個做用就是指定 Worker 的名稱,用來區分多個 Worker 線程。
// 主線程 var myWorker = new Worker('worker.js', { name : 'myWorker' }); // Worker 線程 self.name // myWorker
Worker()
構造函數返回一個 Worker 線程對象,用來供主線程操做 Worker。Worker 線程對象的屬性和方法以下。
Event.data
屬性中。Web Worker 有本身的全局對象,不是主線程的window
,而是一個專門爲 Worker 定製的全局對象。所以定義在window
上面的對象和方法不是所有均可以使用。
Worker 線程有一些本身的全局屬性和方法。
message
事件的監聽函數。此文已由做者受權騰訊雲+社區發佈
搜索關注公衆號「雲加社區」,第一時間獲取技術乾貨,關注後回覆1024 送你一份技術課程大禮包!