狠狠色丁香婷婷综合尤物/久久精品综合一区二区三区/中国有色金属学报/国产日韩欧美在线观看 - 国产一区二区三区四区五区tv

LOGO OA教程 ERP教程 模切知識交流 PMS教程 CRM教程 開發文檔 其他文檔  
 
網站管理員

一文徹底了解Web Worker,十萬條數據都是弟弟

admin
2025年1月6日 10:58 本文熱度 230

今天為大家分享一篇關于web worker的優質文章,讓你了解一下如何通過Web Worker來解決前端處理大量數據運算時頁面假死的問題。

以下是正文:


如何讓前端擁有后端的計算能力,在算力緊缺的年代,擴展前端的業務邊界!

前言

頁面中有十萬條數據,對其進行復雜運算,需要多久呢?

表格4000行,25列,共十萬條數據

運算包括:總和、算術平均、加權平均、最大、最小、計數、樣本標準差、樣本方差、中位數、總體標準差、總體方差

table.jpg

答案是: 35s 左右

注:具體時間根據電腦配置會有所不同

并且 這個時間段內,頁面一直處于假死狀態,對頁面做任何操作都沒有反應??????

boom.gif

什么是假死?

瀏覽器有GUI渲染線程與JS引擎線程,這兩個線程是互斥的關系

當js有大量計算時,會造成 UI 阻塞,出現界面卡頓、掉幀等情況,嚴重時會出現頁面卡死的情況,俗稱假死

致命bug

強行送測吧

測試小姐姐:你的頁面又死了!!
我:還沒有死,在ICU…… ,過一會就好了
測試小姐姐:已經等了好一會了,還不行啊,是個致命bug??
我:……

絕望.jpg

闖蕩前端數十載,竟被提了個致命bug,顏面何在!??

Performance分析假死期間的性能表現

如下圖所示:此次計算總用時為35.45s

重點從以下三個方面分析:

1)FPS

FPS: 表示每秒傳輸幀數,是分析動畫的一個主要性能指標,綠色的長度越長,用戶體驗越好;反之紅色越長,說明卡頓嚴重

從圖中看到FPS中有一條持續了35s的紅線,說明這期間卡頓嚴重

2)火焰圖Main
Main: 表示主線程運行狀況,包括js的計算與執行、css樣式計算、Layout布局等等。

展開Main,紅色倒三角的為Long Task,執行時長50ms就屬于長任務,會阻塞頁面渲染

從圖中看到計算過程的Long Task執行時間為35.45s, 是造成頁面假死的原因

3)Summary 統計匯總面板
Summary: 表示各指標時間占用統計報表

  • Loading: 加載時間
  • Scripting: js計算時間
  • Rendering: 渲染時間
  • Painting: 繪制時間
  • Other: 其他時間
  • Idle: 瀏覽器閑置時間

Scripting代碼執行為35.9s

performance8.png

拿什么拯救你,我的頁面

召喚Web Worker,出來吧神龍

R-C (1).gif

神龍,我想讓頁面的計算變快,并且不卡頓

Web Worker了解一下:

在HTML5的新規范中,實現了 Web Worker 來引入 js 的 “多線程” 技術, 可以讓我們在頁面主運行的 js 線程中,加載運行另外單獨的一個或者多個 js 線程

一句話:Web Worker專門處理復雜計算的,從此讓前端擁有后端的計算能力

在Vue中 使用 Web Worker

1、安裝worker-loader

npm install worker-loader

2、編寫worker.js

onmessage = function (e) {
  // onmessage獲取傳入的初始值
  let sum = e.data;
  for (let i = 0; i < 200000; i++) {
    for (let i = 0; i < 10000; i++) {
      sum += Math.random()
    }
  }
  // 將計算的結果傳遞出去
  postMessage(sum);
}

3、通過行內loader 引入 worker.js

import Worker from "worker-loader!./worker"

4、最終代碼

<template>
    <div>
        <button @click="makeWorker">開始線程</button>
        <!--在計算時 往input輸入值時 沒有發生卡頓-->
        <p><input type="text"></p>
    </div>
</template>

<script>
    import Worker from "worker-loader!./worker";

    export default {
        methods: {
            makeWorker() {
                // 獲取計算開始的時間
                let start = performance.now();
                // 新建一個線程
                let worker = new Worker();
                // 線程之間通過postMessage進行通信
                worker.postMessage(0);
                // 監聽message事件
                worker.addEventListener("message", (e) => {
                    // 關閉線程
                    worker.terminate();
                    // 獲取計算結束的時間
                    let end = performance.now();
                    // 得到總的計算時間
                    let durationTime = end - start;
                    console.log('計算結果:', e.data);
                    console.log(`代碼執行了 ${durationTime} 毫秒`);
                });
            }
        },
    }
</script>

計算過程中,在input框輸入值,頁面一直未發生卡頓

total.png

對比試驗

如果直接把下面這段代碼直接丟到主線程中,計算過程中頁面一直處于假死狀態,input框無法輸入

let sum = 0;
for (let i = 0; i < 200000; i++) {
    for (let i = 0; i < 10000; i++) {
      sum += Math.random()
    }
  }

前戲差不多了,上硬菜

開啟多線程,并行計算

回到要解決的問題,執行多種運算時,給每種運算開啟單獨的線程,線程計算完成后要及時關閉

多線程代碼

<template>
    <div>
        <button @click="makeWorker">開始線程</button>
        <!--在計算時 往input輸入值時 沒有發生卡頓-->
        <p><input type="text"></p>
    </div>
</template>

<script>
    import Worker from "worker-loader!./worker";

    export default {
        data() {
          // 模擬數據
          let arr = new Array(100000).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>

worker.js

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.data
  let value = '';
  switch (type) {
    case 'sum':
      value = formatNumber(sum(arr));
      break
    case 'average':
      value = formatNumber(average(arr));
      break
    case 'weightedAverage':
      value = formatNumber(weightedAverage(arr, weightedList));
      break
    case 'max':
      value = formatNumber(max(arr));
      break
    case 'middleNum':
      value = formatNumber(middleNum(arr));
      break
    case 'min':
      value = formatNumber(min(arr));
      break
    case 'variance':
      value = formatNumber(variance(arr));
      break
    case 'popVariance':
      value = formatNumber(popVariance(arr));
      break
    case 'stdDeviation':
      value = formatNumber(stdDeviation(arr));
      break
    case 'popStandardDeviation':
      value = formatNumber(popStandardDeviation(arr));
      break
    }

  // 發送數據事件
  postMessage({type, value});
}

35s變成6s

從原來的35s變成了最長6s,并且計算過程中全程無卡頓,YYDS

time1.png
src=http___img.soogif.com_n7sySW0OULhVlH5j7OrXHpbqEiM9hDsr.gif&refer=http___img.soogif.gif

最終的效果

table.gif

十萬條太low了,百萬條數據玩一玩

// 修改上文的模擬數據
let arr = new Array(1000000).fill(1).map(() => Math.random()* 10000);
let weightedList = new Array(1000000).fill(1).map(() => Math.random()* 10000);

時間明顯上來了,最長要50多s了,沒事玩一玩,開心就好

time3.png

web worker 提高Canvas運行速度

web worker除了單純進行計算外,還可以結合離屏canvas進行繪圖,提升繪圖的渲染性能和使用體驗

離屏canvas案例

<template>
    <div>
        <button @click="makeWorker">開始繪圖</button>
        <canvas id="myCanvas" width="300" height="150"></canvas>
    </div>
</template>

<script>
    import Worker from "worker-loader!./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>

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="#1989fa";//設置填充顏色
  ctx.fill();//開始填充
  ctx.stroke();
}

效果:

cricle.gif

離屏canvas的優勢

1、對于復雜的canvas繪圖,可以避免阻塞主線程

2、由于這種解耦,OffscreenCanvas的渲染與DOM完全分離了開來,并且比普通Canvas速度提升了一些

Web Worker的限制

1、在 Worker 線程的運行環境中沒有 window 全局對象,也無法訪問 DOM 對象

2、Worker中只能獲取到部分瀏覽器提供的 API,如定時器navigatorlocationXMLHttpRequest

3、由于可以獲取XMLHttpRequest 對象,可以在 Worker 線程中執行ajax請求

4、每個線程運行在完全獨立的環境中,需要通過postMessage、 message事件機制來實現的線程之間的通信

計算時長 超過多長時間 適合用Web Worker

原則上,運算時間超過50ms會造成頁面卡頓,屬于Long task,這種情況就可以考慮使用Web Worker

但還要先考慮通信時長的問題

假如一個運算執行時長為100ms, 但是通信時長為300ms, 用了Web Worker可能會更慢

face.jpg

通信時長

新建一個web worker時, 瀏覽器會加載對應的worker.js資源

下圖中的Time是這個 js 資源的加載時長

load.png

最終標準:

計算的運算時長 - 通信時長 > 50ms,推薦使用Web Worker

場景補充說明

遇到大數據,第一反應: 為什么不讓后端去計算呢?

這里比較特殊,表格4000行,25列
1)用戶可以對表格進行靈活操作,比如刪除任何行或列,選擇或剔除任意行
2)用戶可以靈活選擇運算的類型,計算一個或多個

即便是讓后端計算,需要把大量數據傳給后端,計算好再返回,這個時間也不短,還可能出現用戶頻繁操作,接口數據被覆蓋等情況


總結

Web Worker為前端帶來了后端的計算能力,擴大了前端的業務邊界

可以實現主線程與復雜計運算線程的分離,從而減輕了因大量計算而造成UI阻塞的情況

并且更大程度地利用了終端硬件的性能

本文轉自 https://juejin.cn/post/7137728629986820126

如有侵權,請聯系刪除。


該文章在 2025/1/6 10:58:03 編輯過
關鍵字查詢
相關文章
正在查詢...
點晴ERP是一款針對中小制造業的專業生產管理軟件系統,系統成熟度和易用性得到了國內大量中小企業的青睞。
點晴PMS碼頭管理系統主要針對港口碼頭集裝箱與散貨日常運作、調度、堆場、車隊、財務費用、相關報表等業務管理,結合碼頭的業務特點,圍繞調度、堆場作業而開發的。集技術的先進性、管理的有效性于一體,是物流碼頭及其他港口類企業的高效ERP管理信息系統。
點晴WMS倉儲管理系統提供了貨物產品管理,銷售管理,采購管理,倉儲管理,倉庫管理,保質期管理,貨位管理,庫位管理,生產管理,WMS管理系統,標簽打印,條形碼,二維碼管理,批號管理軟件。
點晴免費OA是一款軟件和通用服務都免費,不限功能、不限時間、不限用戶的免費OA協同辦公管理系統。
Copyright 2010-2025 ClickSun All Rights Reserved