先有問題再有答案
js不是單執行緒嘛 為什麼又說多執行緒?
如何理解H5的多執行緒?
如何理解h5的worker?
web worker 有什麼特點?
有什麼使用場景?
有什麼限制?
多執行緒間是如何通訊?
背景
JavaScript 是單執行緒執行的,這意味著同一個時間只能做一件事情。由於使用者互動、網路請求、IO操作等可能會產生阻塞,所以 JavaScript 引入了非同步機制和事件迴圈的概念。
單執行緒&非同步
非同步可以看做是單執行緒面對需要大量耗時操作時的一種解決方案,它將這些耗時操作非同步處理,也就是掛起這些事件,進行下一步的操作。當非同步事件處理完成後,再透過回撥函式的方式進行處理。所以,非同步並不是在單執行緒之外開闢了新的執行緒,而是透過事件掛起和回撥函式的方式使得 JavaScript 引擎不需要阻塞等待,可以先處理其他任務。
單執行緒&事件迴圈
事件迴圈是 JavaScript 實現非同步的一種方法,它可以解決 JavaScript 單執行緒同步執行帶來的阻塞問題。事件迴圈的機制是,將所有的任務分為宏任務和微任務,JavaScript 引擎先執行一個宏任務,緊接著執行所有的微任務,然後再執行下一個宏任務。這種模式不斷迴圈,稱為事件迴圈
單執行緒&多執行緒
JavaScript是單執行緒的,這是因為JavaScript設計初衷是處理簡單的DOM操作,複雜和耗時的操作可能導致頁面阻塞,影響使用者體驗。
然而,隨著Web應用變得越來越複雜,JavaScript也需要處理更多的複雜且計算密集型任務
。爲了解決這個問題,HTML5提出了Web Workers標準,允許JavaScript建立多個工作執行緒,這些工作執行緒在後臺執行任務,不會影響主執行緒和用戶界面的相應。
我們可以認為,JavaScript的主執行緒仍然是單執行緒的,但是透過Web Workers,JavaScript能夠建立生成真正的作業系統級別的工作執行緒,實現多執行緒程式設計
。
web worker相容性
線上完全可用 無相容性問題。
使用場景
cpu密集型的任務
如果有耗時任務 在主執行緒中長時間佔用cpu,這樣的計算可能會阻塞用戶界面卡頓,導致無法響應使用者的操作。
測試程式碼如下:
const test = () => { let count = 0; for (let i = 0; i < 1000000000; i++) { count = count + i; } }; test();
執行在主執行緒的任務效能分析如圖:
主執行緒耗時12s左右 這期間左側列表是卡死 不能響應使用者滾動的
執行在worker執行緒
效能分析如圖-1
可以看到當選中主執行緒時 js執行僅3毫秒
效能分析如圖-2
當選中worker執行緒時 js執行耗時13s
執行在worker中的整個過程 左側列表是可以響應使用者滾動的, 這就是多執行緒的魅力 開啟了一個工作執行緒 分擔了主執行緒的耗時任務 有效的避免了卡頓。
週期性後臺任務
這部分一般不需要使用者互動,但需要定時執行的任務。這類任務通常包括一些定時獲取伺服器資料、清理快取、自動儲存使用者操作等。 例如高精準大定時器功能。
使用限制
同源限制:
Web Workers 只能載入來自同源的指令碼。也就是說,你不能在你的網站上使用一個來自其他域的指令碼,這是出於安全原因。
不能訪問 DOM
Web Workers 不能訪問頁面的 DOM。這是因為 DOM 操作不能用於多執行緒環境。目的是防止出現兩個執行緒對 DOM 進行修改而造成的資料不同步。
不能訪問某些物件:
Web Workers 不能訪問window物件、document物件、parent物件等, 即使Web Workers不能訪問這些物件,它們仍然可以發起AJAX請求或使用 WebSockets 這類網路API。
使用方式
建立worker
const worker = new Worker('worker.js', { type: 'module', credentials: 'same-origin' });
Worker建構函式接收兩個引數:
url: 這是一個字串值,表示一個指向 JavaScript 檔案的 URL。這個引數是必須的,它的值應該是一個相對於當前 HTML 文件的相對路徑,或者是一個完整的 URL。
options: 這是一個物件,用於配置新建立的 Worker 例項。這個引數是可選的,可以包含以下屬性:
type: 用於指定要建立的 Worker 的型別。支援的值有 "classic" 和 "module"。"classic" 表示 Worker 指令碼是一個普通 JavaScript 檔案,這是預設型別。"module" 表示 Worker 指令碼是一個 JavaScript 模組。
credentials: 用於指定在載入 Worker 指令碼時是否需要證書(cookies)。支援的值有 "omit"、"same-origin" 和 "include"。
Blob物件建立worker
// 建立一個 Blob 物件 const blob = new Blob([ "onmessage = function(e) { postMessage('Worker: ' + e.data); }" ]); // 建立一個 Blob URL const blobURL = window.URL.createObjectURL(blob); // 使用 Blob URL 建立一個 Worker const worker = new Worker(blobURL); worker.onmessage = function(e) { console.log(e.data); // 輸出 "Worker: Hello" } worker.postMessage('Hello'); // 向 worker 傳送訊息
多執行緒間通訊
Worker 執行緒和主執行緒不能直接共享記憶體或變數,它們之間的通訊必須透過訊息傳遞來完成。可以使用 postMessage
方法來發送訊息,並在另一邊使用 onmessage
事件監聽器來接收訊息。
worker.onmessage = function(e) { console.log(e.data); // 輸出 "Worker: Hello" } worker.postMessage('Hello'); // 向 worker 傳送訊息
實現一個執行緒池
本文提供一個透過web worker實現執行緒池的能力。
demo
export const workerStr = ` this.store = {}; this.addEventListener( "message", function (e) { var key = e.data.key; if(e.data.type === 'remove-item' && this.store[key]){ this.store[key] = null; return; } if(e.data.type === 'remove'){ this.store = {}; return; } if (!e.data.task) { return; } var task = new Function('return (' + e.data.task + ')(this.store)'); Promise.resolve(task()) .then((res) => { this.store[key] = { status: "succ", data: res, }; }) .catch((error) => { this.store[key] = { status: "fail", data: error, }; }) .finally(() => { console.log('test worker res', { key, ...this.store[key], }) this.postMessage({ key, ...this.store[key], }); }); }, false ); `; export default workerStr;
import { PoolList, Resolve, TaskRes } from '../type/task'; import workerStr from './worker' let blob: Blob; let url: string; let threadPool: Worker[] = []; /** * 使用webworker開啟一個執行緒池 執行復雜運算 * real parallel * @param list 任務列表 * @param max 最大併發數 預設為2 最好和cpu數量一致 不要設定過多 * @returns 返回一個promise */ export const pool = async (list: PoolList, max = 1) => { if (!blob) { blob = new Blob([workerStr], { type: 'text/plain' }); } if (!url) { url = URL.createObjectURL(blob); } const store: { [key: string]: TaskRes } = {} let _resolve: Resolve; const p = new Promise((resolve) => { _resolve = resolve; }) let doneCount = 0; let index = 0; const runner = (curWorker: Worker) => { const curIndex = index++; if (!curWorker || !list[curIndex]) { return; } const { key, task } = list[curIndex]; curWorker.postMessage({ key, task: `${task}` }); curWorker.onmessage = function (event) { store[event.data.key] = event.data; doneCount++; if (doneCount === list.length) { _resolve(store) return; } runner(curWorker) } curWorker.onerror = function (event) { console.dir('test error', event) } } for (let i = 0; i < max; i++) { let worker = threadPool[i]; if (!worker) { worker = new Worker(url) threadPool[i] = worker; } runner(worker) } return p } /** * clear thread pool */ export const clearPool = () => { threadPool.forEach(worker => { worker.terminate() }) threadPool = []; } export default pool;