<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          前端中如何使用 WebWorker 對用戶體驗進行革命性的提升

          共 2479字,需瀏覽 5分鐘

           ·

          2021-12-09 17:23

          大廠技術(shù)??高級前端??Node進階

          點擊上方?程序員成長指北,關(guān)注公眾號

          回復1,加入高級Node交流群

          前言

          隨著前端應用場景的逐漸復雜化,伴隨而來的對大數(shù)據(jù)的處理就不可避免。那么今天就以一個真實的應用場景為例來談?wù)勄岸酥腥绾瓮ㄟ^子線程來處理大數(shù)據(jù)。

          目前主流顯示器的刷新率為 60Hz,即一幀為 16ms,因此播放動畫時建議小于 16ms,用戶操作響應建議小于 100ms,頁面打開到開始呈現(xiàn)內(nèi)容建議小于 1000ms。

          -- 根據(jù) Chrome 團隊提出的用戶感知性能模型 RAIL。

          以上這段應用是 google 團隊提出的用戶最優(yōu)體驗模型,從 js 運行的角度,大致意思就是盡量保證每一個 js 任務(wù)在最短的時間內(nèi)執(zhí)行完畢。

          案例場景

          現(xiàn)代 web 程序中,要求數(shù)據(jù)、報表導出的需求已經(jīng)非常普遍。導出的數(shù)據(jù)量越來越大、數(shù)據(jù)的復雜程度也越來越高,最常見的時間字段大多數(shù)情況下也可能需要前端去轉(zhuǎn)換,因此對源數(shù)據(jù)的遍歷總避免不了?,F(xiàn)在以導出某站點各類因子的監(jiān)測數(shù)據(jù)報表為例:

          報表格式要求

          1. 每一條數(shù)據(jù)包含 若干項因子數(shù)據(jù),每一個因子項數(shù)據(jù)包含改因子的監(jiān)測數(shù)據(jù)以及對應的評價等級;
          2. 要求導出上一季度 90 天的小時數(shù)據(jù),數(shù)據(jù)源大概在 2100 條左右(有分頁查詢的條件);
          3. 報表要求時間格式為 YYYY年MM月DD日 HH時(例如:2020年12月25日 23時),每一項因子內(nèi)容為 因子數(shù)據(jù) + 因子等級(例如:2.36(I))。

          數(shù)據(jù)源格

          后端返回數(shù)據(jù)格式如下

          {
          ????????"dateTime":?"2021-06-05?14:00:00",
          ????????"name":?"站點一",
          ????????"factorDatas":?[
          ????????????{"code":?"w01010",?"grade":?1,?"value":?26.93},
          ????????????{"code":?"w666666",?"grade":?1,?"value":?1.26}
          ????????]
          }

          復制代碼

          數(shù)據(jù)源基本處理

          對應報表導出需求,對這 2000 多條數(shù)據(jù)的遍歷總避免不了,甚至會有大循環(huán)嵌套小循環(huán)的處理。

          1. 大循環(huán)需要處理 dateTime 字段;
          2. 小循環(huán)中需要循環(huán) factorDatas 字段,查詢 grade 對應的等級名, 最后在拼接出報表需要的格式。

          拋磚引玉

          簡單實現(xiàn)

          以下代碼僅是模擬代碼,默認前端已經(jīng)完成了所有數(shù)據(jù)的加載

          正常的開發(fā)流程當然是采用 for 循環(huán)不斷的調(diào)用分頁的接口不斷地查詢數(shù)據(jù),直到數(shù)據(jù)查詢完畢,然后再進行統(tǒng)一循環(huán)處理每一行數(shù)據(jù)。為方便對數(shù)據(jù)處理單獨將某些公共方法單獨抽一個工具類:

          class?UtilsSerice?{
          ????/**
          ?????*?獲取水質(zhì)類別信息
          ?????*?@param?waterType
          ?????*?@param?keyValue
          ?????*?@param?keyName?
          ?????*/

          ????static?async?getGradeInfo(waterType:?WaterTypeStringEnum,?keyValue:?string?|?number,?keyName?:?string):?Promisenull?|?undefined>?{
          ????????//?緩存中數(shù)據(jù)的?key?
          ????????const?flagId:?string?=?waterType?+?keyValue;
          ????????//?緩存中有對應的值,直接返回
          ????????if?(TEMP_WATER_GRADE_MAP.get(flagId))?{
          ????????????return?TEMP_WATER_GRADE_MAP.get(flagId);
          ????????}
          ????????//?獲取等級列表
          ????????const?gradeList:?WaterGrade[]?=?await?this.getEnvData(waterType);
          ????????//?查詢等級值對應的等級信息
          ????????const?gradeInfo:?WaterGrade?=?gradeList.find((item:?WaterGrade)?=>?{
          ????????????const?valueName:?string?|?number?|?undefined?=?keyName?===?'id'???'id'?:?item.hasOwnProperty('value')???'value'?:?'level';
          ????????????return?item[valueName]?===?keyValue;
          ????????})?as?WaterGrade;
          ????????//?將查詢到的等級信息緩,方便下一次查詢該等級時直接返回
          ????????if?(gradeInfo)?{
          ????????????TEMP_WATER_GRADE_MAP.set(flagId,?gradeInfo);
          ????????}
          ????????return?gradeInfo;
          ????}

          }
          復制代碼

          數(shù)據(jù)導出邏輯如下:

          //?假設(shè)?allList?已經(jīng)是?2100?條數(shù)據(jù)集合
          const?allList?=?[{"dateTime":?"2021-06-05?14:00:00",?"code":?"sssss",?"name":?"站點一",?"factorDatas":?[{"code":?"w01010",?"grade":?1,?"value":?26.93},?{"code":?"w666666",?"grade":?1,?"value":?1.26}]}]

          const?table:?ObjectUnknown[]?=?[];
          for?(let?i?=?0;?i?????const?rows?=?{...allList[i]}
          ????//?按需求處理時間格式
          ????rows['tiemStr']?=?moment(allList[i].dateTime).format('YYYY年MM月DD日?HH時')
          ????for?(let?j?=?0;?j?????????const?code?=?allList[i].factorDatas[j].code
          ????????const?value?=?allList[i].factorDatas[j].value
          ????????const?grade?=?allList[i].factorDatas[j].grade
          ????????//?此處按需求異步獲取等級數(shù)據(jù)----??此方法已經(jīng)盡可能的做了性能優(yōu)化
          ????????const?gradeStr?=?await?UtilsSerice.getGradeInfo('surface',?grade,?'value')
          ????????rows[code]?=?`${value}(${gradeStr})`
          ????}
          ????table.push(rows)
          ????
          }
          const?downConfig:?ExcelDownLoadConfig?=?{
          ????tHeader:?['點位名稱',?'接入編碼',?'監(jiān)測時間',?'因子1',?'因子2',?'因子2'?],
          ????bookType:?'xlsx',
          ????autoWidth:?80,
          ????filename:?`數(shù)據(jù)查詢`,
          ????filterVal:?['name',?'code',?'tiemStr',?'w01010',?'w01011',?'w01012'],
          ????multiHeader:?[],
          ????merges:?[]
          };
          //?此方法是通用的?excel?數(shù)據(jù)處理邏輯
          const?res:?any?=?await?ExcelService.downLoadExcelFileOfMain(table,?downConfig);
          const?file?=?new?Blob([res.data],?{?type:?'application/octet-stream'?});
          //?文件保存
          saveAs(file,?res.filename);
          復制代碼

          由于 JS 引擎線程是單線程且與 GUI 渲染線程是互斥的,因此在執(zhí)行復雜的 js 計算任務(wù)時,用戶的直觀感受就是系統(tǒng)卡頓,例如輸入框無法輸入、動畫停止、按鈕無效等。以上代碼可以實現(xiàn)數(shù)據(jù)的導出,可以看到在主線程導出數(shù)據(jù)時圖片旋轉(zhuǎn)已經(jīng)停止、輸入框已經(jīng)無法輸入。

          main-count-image.gif

          我相信無論多么好說話的甲方,對于這樣的系統(tǒng)估計也是無法接受的。

          問題思考

          稍微有編程經(jīng)驗的開發(fā)多多少少都會明白,是因為大數(shù)據(jù)的 for 循環(huán)遍歷阻塞了其他腳本的執(zhí)行,基于這個思想,有性能優(yōu)化經(jīng)驗的開發(fā)工程師大概率會將這個大遍歷拆分成多個小的任務(wù)來較少卡頓,這種方案也可以一定程度上解決卡頓的問題。但這種時間分片、任務(wù)拆分的優(yōu)化方案并不適合并不是所有的大數(shù)據(jù)處理,尤其是前后數(shù)據(jù)有強依賴關(guān)系的,在這篇文章中暫不探討這種優(yōu)化方案。這篇文章來聊聊 webWorker:

          它允許在 Web 程序中并發(fā)執(zhí)行多個 JavaScript腳本,每個腳本執(zhí)行流都稱為一個線程,彼此間互相獨立,并且有瀏覽器中的 JavaScript引擎負責管理。這將使得線程級別的消息通信成為現(xiàn)實。使得在 Web 頁面中進行多線程編程成為可能。

          -- IMWeb社區(qū)

          webWorker 有幾個特點:

          1. 能夠長時間運行(響應)
          2. 快速啟動和理想的內(nèi)存消耗
          3. 天然的沙箱環(huán)境

          webWorker使用

          創(chuàng)建

          //創(chuàng)建一個Worker對象,并向它傳遞將在新線程中執(zhí)行的腳本url
          const?worker?=?new?Worker('worker.js');
          復制代碼

          通信

          //?發(fā)送消息
          worker.postMessage({first:1,second:2});
          //?監(jiān)聽消息
          worker.onmessage?=?function(event){
          ????console.log(event)
          };
          復制代碼

          銷毀

          主線程中終止worker,此后無法再利用其進行消息傳遞。注意:一旦 terminate 后,無法重新啟用,只能另外創(chuàng)建。

          worker.terminate();
          復制代碼

          導出功能遷移

          接下來聊聊如何把數(shù)據(jù)導出這部分的代碼遷移到 webWorker 中,在功能遷移前,首先需要梳理下數(shù)據(jù)導出的先決條件:

          1:在 webWorker 中需要能調(diào)用 ajax 獲取接口數(shù)據(jù);2:在 webWorker 中要能加載 excel.js 的腳本;3:能正常調(diào)用 file-saver 中的 saveAs 功能;

          基于以上的條件,我們逐一討論,第一點很幸運 webWorker 支持發(fā)起 ajax 請求數(shù)據(jù);第二點 webWorker 中提供了 importScripts() 接口,因此在 webWorker 中也能生成 Excel 的實例;第三點有些遺憾,webWorker 中是無法使用 DOM 對象, 而 file-saver 正好使用了 DOM,因此只能是子線程中處理完數(shù)據(jù)后傳遞數(shù)據(jù)給主線程由主線程執(zhí)行文件保存操作(此處有個小優(yōu)化,后續(xù)講)。

          方案對比

          目前行業(yè)內(nèi)集成 webWorker 的方案有很多,以下簡單做個對比(來自騰訊前端團隊):

          項目簡介構(gòu)建打包底層API封裝跨線程調(diào)用申明可用性監(jiān)控易拓展性
          worker-loader[1]Webpack 官方,源碼打包能力??????
          promise-worker[2]封裝基本 API 為 Promise 化通信??????
          comlink[3]Chrome 團隊, 通信 RPC 封裝???同名函數(shù)(基于Proxy)??
          workerize-loader[4]社區(qū)目前比較完整的方案????同名函數(shù)(基于AST生成)??
          alloy-worker[5]面向事務(wù)的高可用 Worker 通信框架提供構(gòu)建腳本通信?控制器同名函數(shù)(基于約定), TS 聲明完整監(jiān)控指標, 全周期錯誤監(jiān)控命名空間, 事務(wù)生成腳本
          webpack5[6]webpack5 中用于替換 worker-loader提供構(gòu)建腳本????

          基于以上對比和我個人對 ts 的偏愛吧,該案例采用 alloy-worker 來做 webWorker 的集成,由于官方的 npm 包有問題,無法一次到位的集成,所以只能手動集成。

          worker集成

          官方集成文檔[7]

          首先將核心的基礎(chǔ)的 worker 通信源碼復制到項目目錄 src/worker下。

          聲明事務(wù)

          第一步在 src/worker/common/action-type.ts 中添加用于數(shù)據(jù)導出的事務(wù)。

          export?const?enum?TestActionType?{
          ??MessageLog?=?'MessageLog',
          ??//?聲明數(shù)據(jù)導出的事務(wù)
          ??ExportStationReportData?=?'ExportStationReportData'
          ??}
          復制代碼

          請求、響應數(shù)據(jù)類型聲明

          在 src/worker/common/payload-type.ts 文件中聲明請求、響應數(shù)據(jù)類型。

          跨線程通信各事務(wù)的請求數(shù)據(jù)類型聲明

          export?declare?namespace?WorkerPayload?{
          ????namespace?ExcelWorker?{
          ????????//?調(diào)用ExportStationReportData?導出數(shù)據(jù)時需要傳這兩個參數(shù)
          ????????type?ExportStationData?=?{
          ????????????factorList:?SelectOptions[];
          ????????????accessCodes:?string[];
          ????????}?&?Transfer;
          ????}
          }
          復制代碼

          跨線程通信各事務(wù)的響應數(shù)據(jù)類型聲明

          export?declare?namespace?WorkerReponse?{
          ????namespace?ExcelWorker?{
          ????????type?ExportStationData?=?{
          ????????????data:?any;
          ????????}?&?Transfer;
          ????}
          }
          復制代碼

          主線程邏輯

          src/worker/main-thread 下新建 excel.ts 文件,用于編寫數(shù)據(jù)事務(wù)代碼。

          /**
          ?*?第四步:聲明主線程業(yè)務(wù)邏輯代碼
          ?*?TODO
          ?*/

          export?default?class?Excel?extends?BaseAction?{
          ????protected?threadAction:?IMainThreadAction;
          ????/**
          ?????*?導出監(jiān)測點數(shù)據(jù)
          ?????*?@param?payload
          ?????*/

          ????public?async?exportStationReportData(payload?:?WorkerPayload.ExcelWorker.ExportStationData):?Promise?{
          ????????return?this.controller.requestPromise(TestActionType.ExportStationReportData,?payload);
          ????}


          ????protected?addActionHandler():?void?{}
          }
          復制代碼

          主線程邏輯實例化

          src/worker/main-thread/index 中引入 excel;

          主線程聲明事務(wù)命名空間

          //?只聲明事務(wù)命名空間,?用于事務(wù)中調(diào)用其他命名空間的事務(wù)
          export?interface?IMainThreadAction?{
          ????//?....
          ????excel:?Excel;
          }
          復制代碼

          主線程聲明事務(wù)實例化

          export?default?class?MainThreadWorker?implements?IMainThreadAction?{
          ????//?......
          ????public?excel:?Excel;
          ????public?constructor(options:?IAlloyWorkerOptions)?{
          ????????//?.....
          ????????this.excel?=?new?Excel(this.controller,?this);
          ????}
          ????//?........?省略代碼
          }
          復制代碼

          子線程邏輯

          src/worker/worker-thread 下新建 excel.ts 文件,用于編寫數(shù)據(jù)事務(wù)代碼,此文件中是核心的數(shù)據(jù)導出功能。

          數(shù)據(jù)請求、數(shù)據(jù)處理

          export?default?class?Test?extends?BaseAction?{
          ????protected?threadAction:?IWorkerThreadAction;

          ????protected?addActionHandler():?void?{
          ????????this.controller.addActionHandler(TestActionType.ExportStationReportData,?this.exportStationReportData.bind(this));
          ????}
          ????/**
          ?????*?獲取數(shù)據(jù)查詢
          ?????*?@protected
          ?????*/

          ????@HttpGet('/list')
          ????protected?async?getDataList(@HttpParams()?queryDataParams:?QueryDataParams,?factors?:?SelectOptions[],?@HttpRes()?res?:?any):?Promise<{?total:?number;?list:?TableRow[]?}>?{
          ????????return?{list:?res.rows}
          ????}

          ????/**
          ?????*?測試導出數(shù)據(jù)
          ?????*?@private
          ?????*/

          ????private?async?exportExcel(payload?:?WorkerPayload.ExcelWorker.ExportExcel):?Promise?{
          ????????try?{
          ????????????//?worker?中引入?xlsx
          ????????????importScripts('https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.16.9/xlsx.core.min.js');
          ????????????const?table:?ObjectUnknown[]?=?[];
          ????????????for?(let?i?=?0;?i?????????????????const?rows?=?{...allList[i]}
          ????????????????//?按需求處理時間格式
          ????????????????rows['tiemStr']?=?moment(allList[i].dateTime).format('YYYY年MM月DD日?HH時')
          ????????????????for?(let?j?=?0;?j?????????????????????const?code?=?allList[i].factorDatas[j].code
          ????????????????????const?value?=?allList[i].factorDatas[j].value
          ????????????????????const?grade?=?allList[i].factorDatas[j].grade
          ????????????????????//?此處按需求異步獲取等級數(shù)據(jù)----??此方法已經(jīng)盡可能的做了性能優(yōu)化
          ????????????????????const?gradeStr?=?await?UtilsSerice.getGradeInfo('surface',?grade,?'value')
          ????????????????????rows[code]?=?`${value}(${gradeStr})`
          ????????????????}
          ????????????????table.push(rows)

          ????????????}
          ????????????const?downConfig:?ExcelDownLoadConfig?=?{
          ????????????????tHeader:?['點位名稱',?'接入編碼',?'監(jiān)測時間',?'因子1',?'因子2',?'因子2'?],
          ????????????????bookType:?'xlsx',
          ????????????????autoWidth:?80,
          ????????????????filename:?`數(shù)據(jù)查詢`,
          ????????????????filterVal:?['name',?'code',?'tiemStr',?'w01010',?'w01011',?'w01012'],
          ????????????????multiHeader:?[],
          ????????????????merges:?[]
          ????????????};
          ????????????const?res?=?await?ExcelService.downLoadExcelFile(table,?downConfig,?(self?as?any).XLSX);
          ????????????//?由于之前提到的?worker?局限性(無法訪問?DOM)?因此子線程中處理完?excel?所所需的對象后?將數(shù)據(jù)傳遞給主線程,由主線程進行數(shù)據(jù)導出
          ????????????//??普通?postMessage?時會進行?樹的克隆,但此處處理完的數(shù)據(jù)可能會非常大,估計直接將進行?transfer?傳輸數(shù)據(jù)
          ????????????return?{
          ????????????????transferProps:?['data'],
          ????????????????data:?res.data,
          ????????????????filename:?res.filename,
          ????????????}
          ????????}?catch?(e)?{
          ????????????console.log(e);
          ????????}
          ????}
          }

          復制代碼

          子線程邏輯實例化

          src/worker/worker-thread/index 中引入 excel;

          主線程聲明事務(wù)命名空間

          //?只聲明事務(wù)命名空間,?用于事務(wù)中調(diào)用其他命名空間的事務(wù)
          export?interface?IWorkerThreadAction?{
          ????//?....
          ????excel:?Excel;
          }

          復制代碼

          子線程聲明事務(wù)實例化

          class?WorkerThreadWorker?implements?IWorkerThreadAction?{
          ????public?excel:?Excel
          ????//?...?省略代碼
          ????public?constructor()?{
          ????????this.controller?=?new?Controller();
          ????????this.excel?=?new?Excel(this.controller,?this);

          ????????//?...?省略代碼
          ????}
          }
          復制代碼

          至此,導出功能已完整遷移到子線程中。

          主線程調(diào)用

          主線程調(diào)用數(shù)據(jù)導出功能也很簡單,首先實例化一個子線程,然后就可以愉快的將復雜的計算邏輯丟給子線程了,類似于這樣。

          class?HomPage?extends?VueComponent?{
          ????public?created()?{
          ????????try?{
          ????????????//?實例化一個子線程,并將其掛載在?window?上
          ????????????const?alloyWorker?=?createAlloyWorker({
          ????????????????workerName:?'alloyWorker--test',
          ????????????????isDebugMode:?true
          ????????????});
          ????????}catch?(e)?{
          ????????????console.log(e);
          ????????}

          ????}
          ????/**
          ?????*?子線程數(shù)據(jù)導出
          ?????*?@private
          ?????*/

          ????private?async?exportExcelFile()?{
          ????????//?直接調(diào)用申明的方法就可以
          ????????(window?as?any).alloyWorker.excel.exportStationReportData({
          ????????????factorList:?factors,
          ????????????accessCodes:?[{?accessCode:?'sss',?name:?'測試監(jiān)測點'?}]
          ????????}).then((res:?any)?=>?{
          ????????????//?大數(shù)據(jù)導出效果,子線程傳回來的數(shù)據(jù)
          ????????????console.log(res);
          ????????????//?將子線程傳回來的二進制數(shù)據(jù)轉(zhuǎn)換為?Blob?方便文件保存
          ????????????const?file?=?new?Blob([res.data],?{?type:?'application/octet-stream'?});
          ????????????//?保存文件
          ????????????saveAs(file,?res.filename);
          ????????});
          ????}
          }

          復制代碼

          效果如下,可以明確感受到數(shù)據(jù)導出過程中,頁面沒有絲毫的卡頓之感。

          child-count-image.gif

          總結(jié)

          以上代碼中以一個真實的需求案例驗證了 webWorker 對用戶體驗的提升是非常大的。這種需求在大多數(shù)的開發(fā)中可能也不多,但偶爾也會有。當然 webWorker 也并非是唯一解,在同等計算量的情況下,在子線程中做計算并不會比主線程快多少, 甚至會比主線程慢,因此只能將一些對及時反饋要求不高的計算放到子線程中計算。如果想單純的提高計算效率,那只能從算法上入手或者使用 WebAssembly 來提高計算效率,關(guān)于 WebAssembly 在后續(xù)中可以再講講。

          關(guān)于本文

          來自:殘月公子

          https://juejin.cn/post/6970336963647766559

          Node 社群


          我組建了一個氛圍特別好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你對Node.js學習感興趣的話(后續(xù)有計劃也可以),我們可以一起進行Node.js相關(guān)的交流、學習、共建。下方加 考拉 好友回復「Node」即可。


          ???“分享、點贊、在看” 支持一波??

          瀏覽 51
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  欧老太做爱 亚洲性猛交 | 国产精品久久久久久久久午夜福利 | 婷婷五月天小说 | 亚洲欧美电影 | 亚洲无码影视 |