提到 HTML5 總是讓人津津樂道,太多的特性和有趣的 API 讓人耳目一新。但是很多童鞋還停留在語義化的階段,忽視了 HTML5 的強(qiáng)勁之處。
這節(jié)我們來探討一下多線程 Web-Worker。
一、明確 JavaScript 是單線程
JavaScript 語言的一大特點(diǎn)就是單線程,也就是說,同一個(gè)時(shí)間只能做一件事。
聽起來有些匪夷所思,為什么不設(shè)計(jì)成多線程提高效率呢?我們可以假設(shè)一種場景:
假定 JavaScript
同時(shí)有兩個(gè)線程,一個(gè)線程在某個(gè) DOM
節(jié)點(diǎn)上添加內(nèi)容,另一個(gè)線程刪除了這個(gè)節(jié)點(diǎn),這時(shí)瀏覽器應(yīng)該以哪個(gè)線程為準(zhǔn)?
作為瀏覽器腳本語言, JavaScript
的主要用途是與用戶互動(dòng),以及操作 DOM
。
這決定了它只能是單線程,否則會(huì)帶來很復(fù)雜的同步問題。為了避免復(fù)雜性,從一誕生, JavaScript
就是單線程,這已經(jīng)成了這門語言的核心特征,估計(jì)短期內(nèi)很難改變。
二、新曙光:Web Worker
單線程始終是一個(gè)痛點(diǎn),為了利用多核 CPU
的計(jì)算能力, HTML5
提出 Web Worker
標(biāo)準(zhǔn),允許 JavaScript
腳本創(chuàng)建多個(gè)線程。但是子線程完全受主線程控制,且不得操作 DOM
。
所以,這個(gè)新標(biāo)準(zhǔn)并沒有改變 JavaScript
單線程的本質(zhì)。
Web Workers
是現(xiàn)代瀏覽器提供的一個(gè) JavaScript
多線程解決方案,我們可以找到很多使用場景:
1.我們可以用 Web Worker
做一些大計(jì)算量的操作;
2.可以實(shí)現(xiàn)輪詢,改變某些狀態(tài);
3.頁頭消息狀態(tài)更新,比如頁頭的消息個(gè)數(shù)通知;
4.高頻用戶交互,拼寫檢查,譬如:根據(jù)用戶的輸入習(xí)慣、歷史記錄以及緩存等信息來協(xié)助用戶完成輸入的糾錯(cuò)、校正功能等
5.加密:加密有時(shí)候會(huì)非常地耗時(shí),特別是如果當(dāng)你需要經(jīng)常加密很多數(shù)據(jù)的時(shí)候(比如,發(fā)往服務(wù)器前加密數(shù)據(jù))。
6.預(yù)取數(shù)據(jù):為了優(yōu)化網(wǎng)站或者網(wǎng)絡(luò)應(yīng)用及提升數(shù)據(jù)加載時(shí)間,你可以使用 Workers
來提前加載部分?jǐn)?shù)據(jù)以備不時(shí)之需。
加密是一個(gè)使用 Web Worker
的絕佳場景,因?yàn)樗⒉恍枰L問 DOM
或者利用其它魔法,它只是純粹使用算法進(jìn)行計(jì)算而已。隨著大眾對(duì)個(gè)人敏感數(shù)據(jù)的日益重視,信息安全和加密也成為重中之重。這可以從近期的 12306 用戶數(shù)據(jù)泄露事件中體現(xiàn)出來。
一旦在 Worker 進(jìn)行計(jì)算,它對(duì)于用戶來說是無縫地且不會(huì)影響到用戶體驗(yàn)。
三、兼容性
四、基本概念
1.首先記得去判斷是否支持
if (window.Worker) {
...
}
2.創(chuàng)建一個(gè)新的 worker
很簡單
const myWorker = new Worker('worker.js');
postMessage() 方法和 onmessage 事件處理函數(shù)是 Workers 的黑魔法。
3. postMessage
用來發(fā)送消息,而 onmessage
用來監(jiān)聽消息
const worker = new Worker('src/worker.js');
worker.onmessage = e => {
console.log(e.data);
};
worker.postMessage('你好嗎!');
在主線程中使用時(shí), onmessage
和 postMessage()
必須掛在 worker
對(duì)象上,而在 worker
中使用時(shí)不用這樣做。原因是,在 worker
內(nèi)部, worker
是有效的全局作用域。
4.異常處理:
worker.onerror = function(error) {
console.log(error.message);
throw error;
};
5.終止 worker
worker
線程會(huì)被立即殺死,不會(huì)有任何機(jī)會(huì)讓它完成自己的操作或清理工作。
6.在 worker
線程中, workers
也可以調(diào)用自己的 close
方法進(jìn)行關(guān)閉:
五、快速開始
為了快速掌握,我們來做一個(gè)小例子:項(xiàng)目結(jié)構(gòu)如下
├── index.html
└── src
├── main.js
└── worker.js
Html
<html>
<head>
<title>Web Work Demo</title>
<meta charset="UTF-8" />
</head>
<body>
<div id="app"> Hello Jartto! </div>
<script src="src/main.js"></script>
</body>
</html>
main.js
const worker = new Worker('src/worker.js');
worker.onmessage = e => {
const message = e.data;
console.log(`[From Worker]: ${message}`);
document.getElementById('app').innerHTML = message;
};
worker.postMessage('寫的真好!');
Work.js
onmessage = e => {
const message = e.data;
console.log(`[From Main]: ${message}`);
if(message.indexOf('好') > -1) {
postMessage('謝謝支持');
}
};
代碼很簡單,主線程發(fā)送:「寫的真好!」
web worker 收到消息,發(fā)現(xiàn)內(nèi)容中含有「好」字,回傳給主線程:「謝謝支持」
六、局限性
1.在 worker
內(nèi),不能直接操作 DOM
節(jié)點(diǎn),也不能使用 window
對(duì)象的默認(rèn)方法和屬性。然而我們可以使用大量 window
對(duì)象之下的東西,包括 WebSockets
, IndexedDB
以及 FireFox OS
專用的 Data Store API
等數(shù)據(jù)存儲(chǔ)機(jī)制。
這里舉個(gè)例子,我們修改 main.js
:
const worker = new Worker('src/worker.js');
worker.onmessage = e => {
const message = e.data;
console.log(`[From Worker]: ${message}`);
document.getElementById('app').innerHTML = message;
};
+ worker.onerror = function(error) {
+ console.log(error);
+ worker.terminate();
+ };
worker.postMessage('寫的真好!');
再來修改 work.js
+ alert('jartto');
onmessage = e => {
const message = e.data;
console.log(`[From Main]: ${message}`);
if(message.indexOf('好') > -1) {
postMessage('謝謝支持');
}
};
這時(shí)候運(yùn)行就會(huì)報(bào)出:
這是因?yàn)椋?worker.js
執(zhí)行的上下文,與主頁面 HTML
執(zhí)行時(shí)的上下文并不相同,最頂層的對(duì)象并不是 Window
, woker.js
執(zhí)行的全局上下文,而是 WorkerGlobalScope
,我們具體說明。
2. workers
和主線程間的數(shù)據(jù)傳遞通過這樣的消息機(jī)制進(jìn)行:雙方都使用 postMessage()
方法發(fā)送各自的消息,使用 onmessage
事件處理函數(shù)來響應(yīng)消息(消息被包含在 Message
事件的 data
屬性中)。
這個(gè)過程中數(shù)據(jù)并不是被共享而是被復(fù)制。
3.同源限制
分配給 Worker
線程運(yùn)行的腳本文件,必須與主線程的腳本文件同源。
4.文件限制
Worker
線程無法讀取本地文件,即不能打開本機(jī)的文件系統(tǒng) (file://)
,它所加載的腳本,必須來自服務(wù)器。
5.不允許本地文件
Uncaught SecurityError: Failed to create a worker:
script at '(path)/worker.js'
cannot be accessed from origin 'null'.
Chrome doesn’t let you load web workers when running scripts from a local file.
那如何解決呢?我們可以啟動(dòng)一個(gè)本地服務(wù)器,建議使用 http-server
,簡單易用。
6.內(nèi)容安全策略
有別于創(chuàng)建它的 document
對(duì)象, worker
有它自己的執(zhí)行上下文。因此普遍來說, worker
并不受限于創(chuàng)建它的 document
(或者父級(jí) worker
)的內(nèi)容安全策略。
我們來舉個(gè)例子,假設(shè)一個(gè) document
有如下頭部聲明:
Content-Security-Policy: script-src 'self'
這個(gè)聲明有一部分作用在于,禁止它內(nèi)部包含的腳本代碼使用 eval()
方法。然而,如果腳本代碼創(chuàng)建了一個(gè) worker
,在 worker
上下文中執(zhí)行的代碼卻是可以使用 eval()
的。
為了給 worker 指定 CSP,必須為發(fā)送 worker 代碼的請(qǐng)求本身加上一個(gè) CSP。
有一個(gè)例外情況,即 worker
腳本的源如果是一個(gè)全局性的唯一的標(biāo)識(shí)符(例如,它的 URL
指定了數(shù)據(jù)模式或者 blob
), worker
則會(huì)繼承創(chuàng)建它的 document
或者 worker
的 CSP
。
七、擴(kuò)展:WorkerGlobalScope
關(guān)于 ,我們可以在 MDN
上面找到文檔:
1. self
:
我們可以使用 WorkerGlobalScope
的 self
屬性來獲取這個(gè)對(duì)象本身的引用。
2. location
:
location
屬性返回當(dāng)線程被創(chuàng)建出來的時(shí)候與之關(guān)聯(lián)的 WorkerLocation
對(duì)象,它表示用于初始化這個(gè)工作線程的腳步資源的絕對(duì) URL
,即使頁面被多次重定向后,這個(gè) URL
資源位置也不會(huì)改變。
3. close
:
關(guān)閉當(dāng)前線程,與 terminate
作用類似。
4. caches
:
當(dāng)前上下文得 CacheStorage
,確保離線可用,同時(shí)可以自定義請(qǐng)求的響應(yīng)。
5. console
:
支持 console
語法。
6. importScripts
我們可以通過 importScripts()
方法通過 url
在 worker
中加載庫函數(shù)。
7. XMLHttpRequest
有了它,才能發(fā)出 Ajax
請(qǐng)求。
8.可以使用:
- setTimeout/setInterval
- addEventListener/postMessage
還有很多 API
可以使用,這里就不一一舉例了。
八、異常處理
當(dāng) worker
出現(xiàn)運(yùn)行中錯(cuò)誤時(shí),它的 onerror
事件處理函數(shù)會(huì)被調(diào)用。它會(huì)收到一個(gè)擴(kuò)展了 ErrorEvent
接口的名為 error
的事件。該事件不會(huì)冒泡并且可以被取消。
為了防止觸發(fā)默認(rèn)動(dòng)作,worker 可以調(diào)用錯(cuò)誤事件的 preventDefault() 方法。
錯(cuò)誤事件我們常用如下這三個(gè)關(guān)鍵信息:
- Message:可讀性良好的錯(cuò)誤消息;
- Filename:發(fā)生錯(cuò)誤的腳本文件名;
- Lineno:發(fā)生錯(cuò)誤時(shí)所在腳本文件的行號(hào);
worker.onerror = function(error) {
console.log(error.message);
throw error;
};
以上就是本文的全部內(nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。