閱讀完本文你將學到如下知識:javascript
閱讀阿寶哥近期熱門文章(感謝掘友的鼓勵與支持🌹🌹🌹):html
下面咱們開始步入正題,爲了讓你們可以更好地理解和掌握 Web Workers,在正式介紹 Web Workers 以前,咱們先來介紹一些與 Web Workers 相關的基礎知識。前端
在介紹進程與線程的概念前,咱們先來看個進程與線程之間關係形象的比喻:java
如上圖所示,進程是一個工廠,它有獨立的資源,線程是工廠中的工人,多個工人協做完成任務,工人之間共享工廠內的資源,好比工廠內的食堂或餐廳。此外,工廠(進程)與工廠(進程)之間是相互獨立的。爲了讓你們可以更直觀地理解進程與線程的區別,咱們繼續來看張圖:webpack
由上圖可知,操做系統會爲每一個進程分配獨立的內存空間,一個進程由一個或多個線程組成,同個進程下的各個線程之間共享程序的內存空間。相信經過前面兩張圖,小夥伴們對進程和線程之間的區別已經有了必定的瞭解,那麼實際狀況是否是這樣呢?這裏咱們打開 macOS 操做系統下的活動監視器,來看一下寫做本文時全部進程的狀態:git
經過上圖可知,咱們經常使用的軟件,好比微信和搜狗輸入法都是一個獨立的進程,擁有不一樣的 PID(進程 ID),並且圖中的每一個進程都含有多個線程,以微信進程爲例,它就含有 「36」 個線程。那麼什麼是進程和線程呢?下面咱們來介紹進程和線程的概念。github
進程(英語:process),是指計算機中已運行的程序。進程曾經是分時系統的基本運做單位。在面向進程設計的系統(如早期的 UNIX,Linux 2.4 及更早的版本)中,進程是程序的基本執行實體;「在面向線程設計的系統(如當代多數操做系統、Linux 2.6 及更新的版本)中,進程自己不是基本運行單位,而是線程的容器。」web
程序自己只是指令、數據及其組織形式的描述,進程纔是程序的真正運行實例。若干進程有可能與同一個程序相關係,且每一個進程皆能夠同步或異步的方式獨立運行。現代計算機系統可在同一段時間內以進程的形式將多個程序加載到存儲器中,並藉由時間共享(或稱時分複用),以在一個處理器上表現出同時運行的感受。ajax
線程(英語:thread)是操做系統可以進行運算調度的最小單位。大部分狀況下,它被包含在進程之中,是進程中的實際運做單位。「一條線程指的是進程中一個單一順序的控制流,一個進程中能夠併發多個線程,每條線程並行執行不一樣的任務。」chrome
線程是獨立調度和分派的基本單位。線程能夠爲操做系統內核調度的內核線程,如 Win32 線程;由用戶進程自行調度的用戶線程,如 Linux 平臺的 POSIX Thread;或者由內核與用戶進程,如 Windows 7 的線程,進行混合調度。
「同一進程中的多條線程將共享該進程中的所有系統資源,如虛擬地址空間,文件描述符和信號處理等等。」 但同一進程中的多個線程有各自的調用棧(call stack),本身的寄存器環境(register context),本身的線程本地存儲(thread-local storage)。一個進程能夠有不少線程,每條線程並行執行不一樣的任務。
若是一個進程只有一個線程,咱們稱之爲單線程。單線程在程序執行時,所走的程序路徑按照連續順序排下來,前面的必須處理好,後面的纔會執行。單線程處理的優勢:同步應用程序的開發比較容易,但因爲須要在上一個任務完成後才能開始新的任務,因此其效率一般比多線程應用程序低。
若是完成同步任務所用的時間比預計時間長,應用程序可能會不響應。針對這個問題,咱們能夠考慮使用多線程,即在進程中使用多個線程,這樣就能夠處理多個任務。
對於 Web 開發者熟悉的 JavaScript 來講,它運行在瀏覽器中,是單線程的,每一個窗口一個 JavaScript 線程,既然是單線程的,在某個特定的時刻,只有特定的代碼可以被執行,其它的代碼會被阻塞。
❝JS 中實際上是沒有線程概念的,所謂的單線程也只是相對於多線程而言。JS 的設計初衷就沒有考慮這些,針對 JS 這種不具有並行任務處理的特性,咱們稱之爲 「單線程」。 —— 來自知乎 「如何證實 JavaScript 是單線程的?」 @雲澹的回答
❞
其實在瀏覽器內核(渲染進程)中除了 JavaScript 引擎線程以外,還含有 GUI 渲染線程、事件觸發線程、定時觸發器線程等。所以對於瀏覽器的渲染進程來講,它是多線程的。接下來咱們來簡單介紹瀏覽器內核。
「瀏覽器最核心的部分是 「Rendering Engine」,即 「渲染引擎」,不過咱們通常習慣將之稱爲 「瀏覽器內核」。」 它主要包括如下線程:
下面咱們來分別介紹渲染進程中的每一個線程。
GUI 渲染線程負責渲染瀏覽器界面,解析 HTML,CSS,構建 DOM 樹和 RenderObject 樹,佈局和繪製等。當界面須要重繪(Repaint)或因爲某種操做引起迴流(Reflow)時,該線程就會執行。
JavaScript 引擎線程負責解析 JavaScript 腳本並運行相關代碼。 JavaScript 引擎一直等待着任務隊列中任務的到來,而後進行處理,一個Tab頁(Renderer 進程)中不管何時都只有一個 JavaScript 線程在運行 JavaScript 程序。
須要注意的是,GUI 渲染線程與 JavaScript 引擎線程是互斥的,因此若是 JavaScript 執行的時間過長,這樣就會形成頁面的渲染不連貫,致使頁面渲染被阻塞。
當一個事件被觸發時該線程會把事件添加到待處理隊列的隊尾,等待 JavaScript 引擎的處理。這些事件能夠是當前執行的代碼塊如定時任務、也可來自瀏覽器內核的其餘線程如鼠標點擊、AJAX 異步請求等,但因爲 JavaScript 引擎是單線程的,全部這些事件都得排隊等待 JavaScript 引擎處理。
瀏覽器定時計數器並非由 JavaScript 引擎計數的,這是由於 JavaScript 引擎是單線程的,若是處於阻塞線程狀態就會影響記計時的準確,因此經過單獨線程來計時並觸發定時是更爲合理的方案。咱們平常開發中經常使用的 setInterval 和 setTimeout 就在該線程中。
在 XMLHttpRequest 在鏈接後是經過瀏覽器新開一個線程請求, 將檢測到狀態變動時,若是設置有回調函數,異步線程就產生狀態變動事件放到 JavaScript 引擎的處理隊列中等待處理。
前面咱們已經知道了,因爲 JavaScript 引擎與 GUI 渲染線程是互斥的,若是 JavaScript 引擎執行了一些計算密集型或高延遲的任務,那麼會致使 GUI 渲染線程被阻塞或拖慢。那麼如何解決這個問題呢?嘿嘿,固然是使用本文的主角 —— Web Workers。
Web Worker 是 HTML5 標準的一部分,這一規範定義了一套 API,它容許一段 JavaScript 程序運行在主線程以外的另一個線程中。Web Worker 的做用,就是爲 JavaScript 創造多線程環境,容許主線程建立 Worker 線程,將一些任務分配給後者運行。
在主線程運行的同時,Worker 線程在後臺運行,二者互不干擾。等到 Worker 線程完成計算任務,再把結果返回給主線程。這樣的好處是,能夠在獨立線程中處理一些計算密集型或高延遲的任務,從而容許主線程(一般是 UI 線程)不會所以被阻塞或拖慢。
(圖片來源:https://thecodersblog.com/web-worker-and-implementation/)
一般狀況下,你能夠在 Worker 線程中運行任意的代碼,但注意存在一些例外狀況,好比:「直接在 worker 線程中操縱 DOM 元素,或使用 window 對象中的某些方法和屬性。」 大部分 window 對象的方法和屬性是可使用的,包括 WebSockets,以及諸如 IndexedDB 和 FireFox OS 中獨有的 Data Store API 這一類數據存儲機制。
下面咱們以 Chrome 和 Opera 所使用的 Blink 渲染引擎爲例,介紹該渲染引擎下 Web Worker 中所支持的經常使用 APIs:
更多信息請參見: Functions and classes available to workers 。
主線程和 Worker 線程相互之間使用 postMessage() 方法來發送信息,而且經過 onmessage 這個事件處理器來接收信息。數據的交互方式爲傳遞副本,而不是直接共享數據。主線程與 Worker 線程的交互方式以下圖所示:
(圖片來源:https://viblo.asia/p/simple-web-workers-workflow-with-webpack-3P0lPkobZox)
除此以外,Worker 還能夠經過 XMLHttpRequest 來訪問網絡,只不過 XMLHttpRequest 對象的 responseXML
和 channel
這兩個屬性的值將老是 null
。
Web Worker 規範中定義了兩類工做線程,分別是專用線程 Dedicated Worker 和共享線程 Shared Worker,其中,Dedicated Worker 只能爲一個頁面所使用,而 Shared Worker 則能夠被多個頁面所共享。
一個專用 Worker 僅僅能被生成它的腳本所使用,其瀏覽器支持狀況以下:
(圖片來源:[https://caniuse.com/#search=Web%20Workers](https://caniuse.com/#search=Web Workers))
須要注意的是,因爲 Web Worker 有同源限制,因此在進行本地調試或運行如下示例的時候,須要先啓動本地服務器,直接使用 file://
協議打開頁面的時候,會拋出如下異常:
Uncaught DOMException: Failed to construct 'Worker':
Script at 'file:///**/*.js' cannot be accessed from origin 'null'. 複製代碼
「index.html」
<!DOCTYPE html>
<html lang="zh-CN"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>專用線程 Dedicated Worker —— Ping/Pong</title> </head> <body> <h3>阿寶哥:專用線程 Dedicated Worker —— Ping/Pong</h3> <script> if (window.Worker) { let worker = new Worker("dw-ping-pong.js"); worker.onmessage = (e) => console.log(`Main: Received message - ${e.data}`); worker.postMessage("PING"); } else { console.log("嗚嗚嗚,不支持 Web Worker"); } </script> </body> </html> 複製代碼
「dw-ping-pong.js」
onmessage = (e) => {
console.log(`Worker: Received message - ${e.data}`); postMessage("PONG"); } 複製代碼
以上代碼成功運行後,瀏覽器控制檯會輸出如下結果:
Worker: Received message - PING Main: Received message - PONG 複製代碼
每一個 Web Worker 均可以建立本身的子 Worker,這容許咱們將任務分散到多個線程。建立子 Worker 也很簡單,具體咱們來看個例子。
「index.html」
<!DOCTYPE html>
<html lang="zh-CN"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>專用線程 Dedicated Sub Worker —— Ping/Pong</title> </head> <body> <h3>阿寶哥:專用線程 Dedicated Sub Worker —— Ping/Pong</h3> <script> if (window.Worker) { let worker = new Worker("dw-ping-pong.js"); worker.onmessage = (e) => console.log(`Main: Received message - ${e.data}`); worker.postMessage("PING"); } else { console.log("嗚嗚嗚,不支持 Web Worker"); } </script> </body> </html> 複製代碼
「dw-ping-pong.js」
onmessage = (e) => {
console.log(`Worker: Received message - ${e.data}`); setTimeout(() => { let worker = new Worker("dw-sub-ping-pong.js"); worker.onmessage = (e) => console.log(`Worker: Received from sub worker - ${e.data}`); worker.postMessage("PING"); }, 1000); postMessage("PONG"); }; 複製代碼
「dw-sub-ping-pong.js」
onmessage = (e) => {
console.log(`Sub Worker: Received message - ${e.data}`); postMessage("PONG"); }; 複製代碼
以上代碼成功運行後,瀏覽器控制檯會輸出如下結果:
Worker: Received message - PING Main: Received message - PONG Sub Worker: Received message - PING Received from sub worker - PONG 複製代碼
其實在 Web Worker 中,咱們也可使用 importScripts
方法將一個或多個腳本同步導入到 Web Worker 的做用域中。一樣咱們來舉個例子。
「index.html」
<!DOCTYPE html>
<html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>專用線程 Dedicated Worker —— importScripts</title> </head> <body> <h3>阿寶哥:專用線程 Dedicated Worker —— importScripts</h3> <script> let worker = new Worker("worker.js"); worker.onmessage = (e) => console.log(`Main: Received kebab case message - ${e.data}`); worker.postMessage( "Hello, My name is semlinker." ); </script> </body> </html> 複製代碼
「worker.js」
importScripts("https://cdn.bootcdn.net/ajax/libs/lodash.js/4.17.15/lodash.min.js");
onmessage = ({ data }) => { postMessage(_.kebabCase(data)); }; 複製代碼
以上代碼成功運行後,瀏覽器控制檯會輸出如下結果:
Main: Received kebab case message - hello-my-name-is-semlinker
複製代碼
在前面的例子中,咱們都是使用外部的 Worker 腳原本建立 Web Worker 對象。其實你也能夠經過 Blob URL 或 Data URL 的形式來建立 Web Worker,這類 Worker 也被稱爲 Inline Worker。
「1. 使用 Blob URL 建立 Inline Worker」
Blob URL/Object URL 是一種僞協議,容許 Blob 和 File 對象用做圖像,下載二進制數據連接等的 URL 源。在瀏覽器中,咱們使用 URL.createObjectURL
方法來建立 Blob URL,該方法接收一個 Blob
對象,併爲其建立一個惟一的 URL,其形式爲 blob:<origin>/<uuid>
,對應的示例以下:
blob:https://example.org/40a5fb5a-d56d-4a33-b4e2-0acf6a8e5f641
複製代碼
瀏覽器內部爲每一個經過 URL.createObjectURL
生成的 URL 存儲了一個 URL → Blob 映射。所以,此類 URL 較短,但能夠訪問 Blob
。生成的 URL 僅在當前文檔打開的狀態下才有效。它容許引用 <img>
、<a>
中的 Blob
,但若是你訪問的 Blob URL 再也不存在,則會從瀏覽器中收到 404 錯誤。
const url = URL.createObjectURL(
new Blob([`postMessage("Dedicated Worker created by Blob")`]) ); let worker = new Worker(url); worker.onmessage = (e) => console.log(`Main: Received message - ${e.data}`); 複製代碼
除了在代碼中使用字符串動態建立 Worker 腳本,也能夠把 Worker 腳本使用類型爲 javascript/worker
的 script
標籤內嵌在頁面中,具體以下所示:
<script id="myWorker" type="javascript/worker"> self['onmessage'] = function(event) { postMessage('Hello, ' + event.data.name + '!'); }; </script> 複製代碼
接着就是經過 script 對象的 textContent
屬性來獲取對應的內容,而後使用 Blob API 和 createObjectURL API 來最終建立 Web Worker:
<script> let workerScript = document.querySelector('#myWorker').textContent; let blob = new Blob(workerScript, {type: "text/javascript"}); let worker = new Worker(URL.createObjectURL(blob)); </script> 複製代碼
「2. 使用 Data URL 建立 Inline Worker」
Data URLs 由四個部分組成:前綴(data:
)、指示數據類型的 MIME 類型、若是非文本則爲可選的 base64
標記、數據自己:
data:[<mediatype>][;base64],<data>
複製代碼
mediatype
是個 MIME 類型的字符串,例如 "image/jpeg
" 表示 JPEG 圖像文件。若是被省略,則默認值爲 text/plain;charset=US-ASCII
。若是數據是文本類型,你能夠直接將文本嵌入(根據文檔類型,使用合適的實體字符或轉義字符)。若是是二進制數據,你能夠將數據進行 base64 編碼以後再進行嵌入。
const url = `data:application/javascript,${encodeURIComponent( `postMessage("Dedicated Worker created by Data URL")` )}`; let worker = new Worker(url); worker.onmessage = (e) => console.log(`Main: Received message - ${e.data}`); 複製代碼
一個共享 Worker 是一種特殊類型的 Worker,能夠被多個瀏覽上下文訪問,好比多個 windows,iframes 和 workers,但這些瀏覽上下文必須同源。相比 dedicated workers,它們擁有不一樣的做用域。其瀏覽器支持狀況以下:
(圖片來源:[https://caniuse.com/#search=Web%20Workers](https://caniuse.com/#search=Web Workers))
與常規的 Worker 不一樣,首先咱們須要使用 onconnect
方法等待鏈接,而後咱們得到一個端口,該端口是咱們與窗口之間的鏈接。
「index.html」
<!DOCTYPE html>
<html lang="zh-CN"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>共享線程 Shared Worker</title> </head> <body> <h3>阿寶哥:共享線程 Shared Worker</h3> <button id="likeBtn">點贊</button> <p>阿寶哥一共收穫了<span id="likedCount">0</span>個👍</p> <script> let likes = 0; let likeBtn = document.querySelector("#likeBtn"); let likedCountEl = document.querySelector("#likedCount"); let worker = new SharedWorker("shared-worker.js"); worker.port.start(); likeBtn.addEventListener("click", function () { worker.port.postMessage("like"); }); worker.port.onmessage = function (val) { likedCountEl.innerHTML = val.data; }; </script> </body> </html> 複製代碼
「shared-worker.js」
let a = 666;
console.log("shared-worker"); onconnect = function (e) { var port = e.ports[0]; port.onmessage = function () { port.postMessage(a++); }; }; 複製代碼
在 Shared Worker 的示例頁面上有一個 「點贊」 按鈕,每次點擊時點贊數會加 1。首先你新開一個窗口,而後點擊幾回。而後新開另外一個窗口繼續點擊,這時你會發現當前頁面顯示的點贊數是基於前一個頁面的點贊數繼續累加。
在實際項目開發過程當中,若須要調試 Shared Workers 中的腳本,能夠經過 chrome://inspect
來進行調試,具體步驟以下圖所示:
Service workers 本質上充當 Web 應用程序與瀏覽器之間的代理服務器,也能夠在網絡可用時做爲瀏覽器和網絡間的代理。它們旨在(除其餘以外)使得可以建立有效的離線體驗,攔截網絡請求並基於網絡是否可用以及更新的資源是否駐留在服務器上來採起適當的動做。
(圖片來源:https://www.pavlompas.com/blog/web-workers-vs-service-workers-vs-worklets)
Service workers 的瀏覽器支持狀況以下:
因爲 Service workers 不是本文的重點,這裏阿寶哥就不展開介紹了,感興趣的小夥伴請自行了解一下。下面咱們開始介紹 Web Workers API。
Worker() 構造函數建立一個 Worker 對象,該對象執行指定的URL腳本。這個腳本必須遵照同源策略 。若是違反同源策略,則會拋出一個 SECURITY_ERR 類型的 DOMException。
Worker 構造函數的語法爲:
const myWorker = new Worker(aURL, options);
複製代碼
相關的參數說明以下:
須要注意的是,在建立 Web Worker 的時候,可能會出現如下異常:
text/javascript
。
「示例」
const worker = new Worker("task.js");
複製代碼
當咱們調用 Worker 構造函數後會返回一個 Worker 線程對象,用來供主線程操做 Worker。Worker 線程對象的屬性和方法以下:
Event.data
屬性中。
下面咱們再來舉一個 Dedicated Worker 的例子:
「index.html」
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Dedicated Worker Demo</title> </head> <body> <h3>Dedicated Worker Demo</h3> <script> const worker = new Worker("task.js"); worker.postMessage({ id: 666, msg: "Hello Semlinker", }); worker.onmessage = function (message) { let data = message.data; console.log(`Main: Message from worker ${JSON.stringify(data)}`); worker.terminate(); }; worker.onerror = function (error) { console.log(error.filename, error.lineno, error.message); }; </script> </body> </html> 複製代碼
「task.js」
而 Dedicated Worker 所執行的代碼以下所示:
onmessage = function (message) {
let data = message.data; console.log(`Worker: Message from main thread ${JSON.stringify(data)}`); data.msg = "Hi from task.js"; postMessage(data); }; 複製代碼
以上代碼成功運行後,控制檯會輸出如下結果:
Worker: Message from main thread {"id": 666,"msg": "Hello Semlinker"} worker-demo.html:20 Main: Message from worker {"id":666, "msg":"Hi from task.js"} 複製代碼
爲了讓你們更好的理解 Web Worker 的工做流程,咱們來了解一下 WebKit 加載並執行 Worker 線程的流程:
(圖片來源:http://www.alloyteam.com/2015/11/deep-in-web-worker/)
看到這裏相信有些小夥伴會好奇,介紹了那麼多 Web Worker 的相關知識,在哪裏能夠直觀地感覺到 Web Worker,接下來咱們將從如下兩個角度來觀察它。
這裏阿寶哥以 Chrome 瀏覽器爲例,首先打開 Chrome 開發者工具,而後選擇 「Sources -> Page」:
打開 Chrome 任務管理器以後,咱們能夠找到當前 Tab 頁對應的進程 ID,即爲 「5194」,接着咱們打開 macOS 下的活動監視器,而後選中 「5194」 進程,而後對該進程進行取樣操做:
取樣完成後,能夠看到當前渲染進程中完整的線程信息,紅框中標出的就是咱們想要找的 「Dedicated Worker」。
原本是想一口氣寫完 「「你不知道的 Web Workers」」,但考慮到部分小夥伴們的感覺,避免出現如下羣友提到的狀況,阿寶哥決定拆成上下兩篇。
下篇阿寶哥將着重介紹 「Web Worker 一些常見的使用場景和 Deno Web Workers 的相關實現」,感興趣的小夥們記得持續關注阿寶哥喲。
閱讀其餘 「「你不知道的 XXX 系列教程」」
建立了一個 「全棧修仙之路交流羣」 的微信羣,想加羣的小夥伴,加我微信 "semlinker",備註 2。阿里、京東、騰訊的大佬都在羣裏等你喲。 semlinker/awesome-typescript 1.6K
本文使用 mdnice 排版