背景
js是單線程這是大家都知道,為了防止多個線程同時操作DOM,這個導致一個復雜的同步問題。比如,假定JavaScript同時有兩個線程,一個線程在某個DOM節點上添加內容,另一個線程刪除了這個節點,這時瀏覽器應該以哪個線程為準。
webWorker
web worker是 HTML5 標準的一部分,這一規范定義了一套 API,允許我們在 js 主線程之外開辟新的 Worker 線程,并將一段 js 腳本運行其中,它賦予了開發者利用 js 操作多線程的能力。
因為是獨立的線程,Worker 線程與 js 主線程能夠同時運行,互不阻塞。所以,在我們有大量運算任務時,可以把運算任務交給 Worker 線程去處理,當 Worker 線程計算完成,再把結果返回給 js 主線程。這樣,js 主線程只用專注處理業務邏輯,不用耗費過多時間去處理大量復雜計算,從而減少了阻塞時間,也提高了運行效率,頁面流暢度和用戶體驗自然而然也提高了。
常見問題
場景一:人臉識別和活體檢測
在這兩個場景中會通過ws實時上傳圖片(加密)
這里面主要是兩個問題:
(1)圖片加密頻繁操作,導致占用主線程,頁面出現卡頓卡死情況
(2)定時器延時,上面操作導致定時器并不是開始規定的5秒可能是7秒8秒
場景二:大規模數據上傳
比如有個項目前端拿到很大量據需要,需要后處理后再上傳服務器。
處理數據可能會占用主線程導致頁面顯示不流程,卡頓等情況
場景三:請求過于頻繁,讀寫操作過多
同上
場景四:視頻音頻轉碼、轉格式、加密等操作。
和場景一一樣,大量耗費計算資源。阻塞主線程,導致頁面流暢度下降,響應降低。
解決以上問題可以考慮使用webwork
怎么使用?
要使用先了解它
Web Worker的限制
1、在 Worker 線程的運行環境中沒有 window 全局對象,也無法訪問 DOM 對象。
2、Worker中只能獲取到部分瀏覽器提供的 API,如定時器、navigator、location、XMLHttpRequest等。
3、由于可以獲取XMLHttpRequest 對象,可以在 Worker 線程中執行ajax請求。
4、每個線程運行在完全獨立的環境中,需要通過postMessage、 message事件機制來實現的線程之間的通信。
worker.postMessage:
向 worker 的內部作用域發送一個消息,消息可由任何 JavaScript 對象組成
worker.terminate:
立即終止 worker。該方法并不會等待 worker 去完成它剩余的操作;worker 將會被立刻停止
worker.onmessage:
當 worker 的父級接收到來自其 worker 的消息時,會在 Worker 對象上觸發message 事件
worker.onerror:
當 worker 出現運行中錯誤時,它的 onerror事件處理函數會被調用。它會收到一個擴展了
ErrorEvent 接口的名為 error 的事件
我直接上代碼,我常用(不,是只會)vue,看在項目是怎么使用的。
我寫了幾個worker.js
1:msg.worker.js?為了消息推送
onmessage = function(e) {console.log(e)postMessage(e.data.num);// close(); }
import MsgWorker from "./demo/msg.worker"; // mounted 初始化 this.msgWorker = new MsgWorker ();this.worker.onmessage = (event) => {console.log(event.data);this.result = event.data;console.log("主線程收到回復,即將關閉worker連接", this.index);// this.worker.terminate(); };methods: {useWorker() {this.msgWorker.postMessage({ text: "當前時間:", num: Date.now() });},},
想想:他能干嘛?
利用策略模式,全局監聽操作,和vue Bus?功能一樣,使用在頁面交互功能一樣。
有人說和寫個全局方法不一樣嗎?
不一樣,它不占用js的主線程
2:time.worker.js添加定時器
onmessage = function (e) {setTimeout(() => {postMessage(Date.now());}, e.data.time);// close(); };
import TimeWorker from "./demo/time.worker";//初始化mounted mounted() {this.timeWorker = new TimeWorker();this.timeWorker.onmessage = (event) => {this.result = event.data;console.log("定時完成");};}, //方法調用methods: {useWorker() {this.timeWorker.postMessage({ time: 10000 });},},
?定時器能干嘛?如果進來先執行?上面定時器useWorker()? 再執行下面計算demo()
?它能定時輸出時間,不被js主線程阻塞影響。demo() {console.log("Start", Date.now());let i = 0;for (let index = 0; index < 100000000000; index++) {i += index;}console.log(i, Date.now())console.log("End", Date.now()); }
3:canvas.worker.js canvas繪制
<template><div class="canvas-demo"><button @click="makeWorker">開始繪圖</button><canvas id="myCanvas" width="300" height="150"></canvas></div> </template><script> import Worker from "./demo/canvas.worker";export default {methods: {makeWorker() {let worker = new Worker();let htmlCanvas = document.getElementById('myCanvas');// 使用canvas的transferControlToOffscreen函數獲取一個OffscreenCanvas對象let offscreen = htmlCanvas.transferControlToOffscreen();// 注意:第二個參數不能省略worker.postMessage({ canvas: offscreen }, [offscreen]);}} }; </script><style lang="less"> .canvas-demo {padding: 20px; } </style>
canvas.worker.js
onmessage = function (e) {// 使用OffscreenCanvas(離屏Canvas)let canvas = e.data.canvas;// 獲取繪圖上下文let ctx = canvas.getContext('2d');// 繪制一個圓弧ctx.beginPath(); // 開啟路徑ctx.arc(150, 75, 50, 0, Math.PI * 2);ctx.fillStyle = '#333333'; //設置填充顏色ctx.fill(); //開始填充ctx.stroke(); };
4:xhrWorker.js接口調用?
<template><div><button @click="useWorker">開始線程</button></div> </template><script> import { fetchApi} from "./demo/xhrWorker.js"; export default {data() {return {worker: null,};},mounted() {const blob = fetchApi();this.worker = new Worker(blob); // 使用上面import進來的js,名字為 demo.worker.worker.js,不可配置,路徑相對比較靈活,需要worker-loaderthis.worker.onmessage = (event) => {console.log(event.data);console.log("主線程收到回復,即將關閉worker連接", event.data);// this.worker.terminate();};this.worker.onerror = (event) => {console.log(event.data);};},methods: {useWorker() {this.worker.postMessage({url: `http://xx.xx.xx.xx:8888/login`,data: {password: "admin",username: "admin123456",},responseType: "json",method: "POST",id: Date.now(),});},},// 頁面關閉,如果還沒有計算完成,要銷毀對應線程beforeDestroy() {}, }; </script>
?xhrWorker.js? 我嘗試將他寫成 xhr.worker.js?結果獲取不到fetchApi方法,可以注意下
export function fetchApi() {const workerCode = ` self.addEventListener('message', async function (e) {const { url, data, responseType, method, id } = e.dataconst xhr = new XMLHttpRequest()xhr.open(method, url, true)xhr.responseType = responseTypexhr.setRequestHeader('Content-Type', 'application/json')xhr.onload = function () {if (xhr.status === 200) {self.postMessage({xhrRes: xhr.response, id})} else {self.postMessage({ error: 'error' })}}xhr.onerror = function () {self.postMessage({ error: 'error' })}xhr.send(JSON.stringify(data)) }) `const blob = new Blob([workerCode], { type: 'application/javascript' })const blobUrl = URL.createObjectURL(blob)return blobUrl }
?5:dataWorker.js數據計算?
<template><div><div class="data-lsit"><div class="=data-item" v><span>數據</span></div></div><button @click="makeWorker">開始線程</button><!--在計算時 往input輸入值時 沒有發生卡頓--><p><input type="text" /></p></div> </template><script> import Worker from "./demo/math.worker";export default {data() {// 模擬數據let arr = new Array(20).fill(1).map(() => Math.random() * 10000);let weightedList = new Array(100000).fill(1).map(() => Math.random() * 10000);let calcList = [{ type: "sum", name: "總和" },{ type: "average", name: "算術平均" },{ type: "weightedAverage", name: "加權平均" },{ type: "max", name: "最大" },{ type: "middleNum", name: "中位數" },{ type: "min", name: "最小" },{ type: "variance", name: "樣本方差" },{ type: "popVariance", name: "總體方差" },{ type: "stdDeviation", name: "樣本標準差" },{ type: "popStandardDeviation", name: "總體標準差" },];return {workerList: [], // 用來存儲所有的線程calcList, // 計算類型arr, // 數據weightedList, // 加權因子};},methods: {makeWorker() {this.calcList.forEach((item) => {let workerName = `worker${this.workerList.length}`;let worker = new Worker();let start = performance.now();worker.postMessage({arr: this.arr,type: item.type,weightedList: this.weightedList,});worker.addEventListener("message", (e) => {worker.terminate();let tastName = "";this.calcList.forEach((item) => {if (item.type === e.data.type) {item.value = e.data.value;tastName = item.name;}});let end = performance.now();let duration = end - start;console.log(`當前任務: ${tastName}, 計算用時: ${duration} 毫秒`);});this.workerList.push({ [workerName]: worker });});},clearWorker() {if (this.workerList.length > 0) {this.workerList.forEach((item, key) => {item[`worker${key}`].terminate && item[`worker${key}`].terminate(); // 終止所有線程});}},},// 頁面關閉,如果還沒有計算完成,要銷毀對應線程beforeDestroy() {this.clearWorker();}, }; </script>
import { create, all } from 'mathjs' const config = {number: 'BigNumber',precision: 20 // 精度 } const math = create(all, config);//加 const numberAdd = (arg1,arg2) => {return math.number(math.add(math.bignumber(arg1), math.bignumber(arg2))); } //減 const numberSub = (arg1,arg2) => {return math.number(math.subtract(math.bignumber(arg1), math.bignumber(arg2))); } //乘 const numberMultiply = (arg1, arg2) => {return math.number(math.multiply(math.bignumber(arg1), math.bignumber(arg2))); } //除 const numberDivide = (arg1, arg2) => {return math.number(math.divide(math.bignumber(arg1), math.bignumber(arg2))); }// 數組總體標準差公式 const popVariance = (arr) => {return Math.sqrt(popStandardDeviation(arr)) }// 數組總體方差公式 const popStandardDeviation = (arr) => {let s,ave,sum = 0,sums= 0,len = arr.length;for (let i = 0; i < len; i++) {sum = numberAdd(Number(arr[i]), sum);}ave = numberDivide(sum, len);for(let i = 0; i < len; i++) {sums = numberAdd(sums, numberMultiply(numberSub(Number(arr[i]), ave), numberSub(Number(arr[i]), ave)))}s = numberDivide(sums,len)return s; }// 數組加權公式 const weightedAverage = (arr1, arr2) => { // arr1: 計算列,arr2: 選擇的權重列let s,sum = 0, // 分子的值sums= 0, // 分母的值len = arr1.length;for (let i = 0; i < len; i++) {sum = numberAdd(numberMultiply(Number(arr1[i]), Number(arr2[i])), sum);sums = numberAdd(Number(arr2[i]), sums);}s = numberDivide(sum,sums)return s; }// 數組樣本方差公式 const variance = (arr) => {let s,ave,sum = 0,sums= 0,len = arr.length;for (let i = 0; i < len; i++) {sum = numberAdd(Number(arr[i]), sum);}ave = numberDivide(sum, len);for(let i = 0; i < len; i++) {sums = numberAdd(sums, numberMultiply(numberSub(Number(arr[i]), ave), numberSub(Number(arr[i]), ave)))}s = numberDivide(sums,(len-1))return s; }// 數組中位數 const middleNum = (arr) => {arr.sort((a,b) => a - b)if(arr.length%2 === 0){ //判斷數字個數是奇數還是偶數return numberDivide(numberAdd(arr[arr.length/2-1], arr[arr.length/2]),2);//偶數個取中間兩個數的平均數}else{return arr[(arr.length+1)/2-1];//奇數個取最中間那個數} }// 數組求和 const sum = (arr) => {let sum = 0, len = arr.length;for (let i = 0; i < len; i++) {sum = numberAdd(Number(arr[i]), sum);}return sum; }// 數組平均值 const average = (arr) => {return numberDivide(sum(arr), arr.length) }// 數組最大值 const max = (arr) => {let max = arr[0]for (let i = 0; i < arr.length; i++) {if(max < arr[i]) {max = arr[i]}}return max }// 數組最小值 const min = (arr) => {let min = arr[0]for (let i = 0; i < arr.length; i++) {if(min > arr[i]) {min = arr[i]}}return min }// 數組有效數據長度 const count = (arr) => {let remove = ['', ' ', null , undefined, '-']; // 排除無效的數據return arr.filter(item => !remove.includes(item)).length }// 數組樣本標準差公式 const stdDeviation = (arr) => {return Math.sqrt(variance(arr)) }// 數字三位加逗號,保留兩位小數 const formatNumber = (num, pointNum = 2) => {if ((!num && num !== 0) || num == '-') return '--'let arr = (typeof num == 'string' ? parseFloat(num) : num).toFixed(pointNum).split('.')let intNum = arr[0].replace(/\d{1,3}(?=(\d{3})+(.\d*)?$)/g,'$&,')return arr[1] === undefined ? intNum : `${intNum}.${arr[1]}` }onmessage = function (e) {let {arr, type, weightedList} = e.datalet value = '';switch (type) {case 'sum':value = formatNumber(sum(arr));breakcase 'average':value = formatNumber(average(arr));breakcase 'weightedAverage':value = formatNumber(weightedAverage(arr, weightedList));breakcase 'max':value = formatNumber(max(arr));breakcase 'middleNum':value = formatNumber(middleNum(arr));breakcase 'min':value = formatNumber(min(arr));breakcase 'variance':value = formatNumber(variance(arr));breakcase 'popVariance':value = formatNumber(popVariance(arr));breakcase 'stdDeviation':value = formatNumber(stdDeviation(arr));breakcase 'popStandardDeviation':value = formatNumber(popStandardDeviation(arr));break}// 發送數據事件postMessage({type, value}); }
總結
以上列子分了五個場景,直接引入項目就可以測試,最后一個例子請參考:一文徹底了解Web Worker,十萬條數據都是弟弟附帶源碼。
寫這篇文章為了鞏固下webWorker的使用,希望能對你們有所幫助,如果有幫助請點個贊。謝了