目錄
前言
引入 Web Worker
Worker 實(shí)踐
Worker 到底有多難用
類庫(kù)調(diào)研
有類庫(kù)加持的 worker 現(xiàn)狀
向著舒適無(wú)感的 worker 編寫前進(jìn)
1. 抽取依賴,管理編譯和更新:
2. 定義公共調(diào)用函數(shù),引入所打包的依賴并串聯(lián)流程:
3. 優(yōu)化語(yǔ)法支持
4. 其他問題
總結(jié)
參考資料
前言
????Web Workers
?是 2009 年就已經(jīng)提案的老技術(shù),但是在很多項(xiàng)目中的應(yīng)用相對(duì)較少,常見一些文章討論如何寫 demo ,但很少有工程化和項(xiàng)目級(jí)別的實(shí)踐,本文會(huì)結(jié)合?Web Workers
?在京東羚瓏的程序化設(shè)計(jì)項(xiàng)目中的實(shí)踐,分享一下在當(dāng)下的 2023 年,關(guān)于?worker
?融入項(xiàng)目的一些思考和具體的實(shí)現(xiàn)方式,涉及到的 demo 已經(jīng)放在 github 上附在文末,可供參考。
????????先簡(jiǎn)單介紹下?Web Workers
,它是一種可以運(yùn)行在 Web 應(yīng)用程序后臺(tái)線程,獨(dú)立于主線程之外的技術(shù)。眾所周知,JavaScript 語(yǔ)言是單線程模型的,而通過使用?Web Workers
,我們可以創(chuàng)造多線程環(huán)境,從而可以發(fā)揮現(xiàn)代計(jì)算機(jī)的多核 CPU 能力,在應(yīng)對(duì)規(guī)模越來(lái)越大的 Web 程序時(shí)也有較多收益。
????????Web Workers 宏觀語(yǔ)義上包含了三種不同的 Worker:DedicatedWorker(專有worker)
、?SharedWorker(共享Worker)
、?ServiceWorker
,本文討論的是第一種,其他兩種大家可以自行研究一下。
引入 Web Worker
當(dāng)引入新技術(shù)時(shí),通常我們會(huì)考慮的問題有:
????????1、兼容性如何?
????????2、使用場(chǎng)景在哪?
問題 1,Web Workers 是 2009 年的提案,2012 年各大瀏覽器已經(jīng)基本支持,11 年過去了,現(xiàn)在使用已經(jīng)完全沒有問題啦
問題 2,主要考慮了以下 3 點(diǎn):
-
·Worker API
?的局限性:同源限制、無(wú) DOM 對(duì)象、異步通信,因此適合不涉及 DOM 操作的任務(wù) -
·Worker
?的使用成本:創(chuàng)建時(shí)間 + 數(shù)據(jù)傳輸時(shí)間;考慮到可以預(yù)創(chuàng)建,可以忽略創(chuàng)建時(shí)間,只考慮數(shù)據(jù)傳輸成本,這里可參考 19 年的一個(gè)測(cè)試?Is postMessage slow[1]?,簡(jiǎn)要結(jié)論是比較樂觀的,大部分設(shè)備和數(shù)據(jù)情況下速度不是瓶頸 -
·任務(wù)特點(diǎn):需要是可并行的多任務(wù),為了充分利用多核能力,可并行的任務(wù)數(shù)越接近 CPU 數(shù)量,收益會(huì)越高。多線程場(chǎng)景的收益計(jì)算,可以參考?
Amdahl
?公式,其中?F
?是初始化所需比例,N
?是可并行數(shù):
綜上結(jié)論是,可并行的計(jì)算密集型任務(wù)適合用?Worker
?來(lái)做。
不過 github 上我搜羅了一圈,也發(fā)現(xiàn)有一些不局限于此,頗有創(chuàng)意的項(xiàng)目,供大家打開思路:
-
·redux 挪到了 worker 內(nèi)[2]
-
·dom 挪到了 worker 內(nèi)[3]
-
·可使用多核能力的框架[4]
Worker 實(shí)踐
????????介紹完?worker
?,一個(gè)問題出現(xiàn)了:為什么一個(gè)兼容性良好,能夠發(fā)揮并發(fā)能力的技術(shù)(聽起來(lái)很有誘惑力),到現(xiàn)在還沒有大規(guī)模使用呢?
????????我理解有 2 個(gè)原因:一是暫無(wú)匹配度完美的使用場(chǎng)景,因此引入被擱置了;二是?worker api
?設(shè)計(jì)得太難用,參考很多 demo 看,限制多配置還麻煩,讓人望而卻步。本文會(huì)主要著力于第二點(diǎn),希望給大家的?worker
?實(shí)踐提供一些成熟的工程化思路。
????????至于第一點(diǎn)理由,在如此卷的前端領(lǐng)域,當(dāng)你手中已經(jīng)有了一把好用的錘子,還找不到那顆需要砸的釘子嗎?
Worker 到底有多難用
下面是一個(gè)原始?worker
?的調(diào)用示例,上面是主線程文件,下面是?worker
?文件:
//?index.js
const?worker?=?new?Worker('./worker.js')
worker.onmessage?=?function?(messageEvent)?{
??console.log(messageEvent)
}
//?worker.js
importScripts('constant.js')
function?a()?{
??console.log('test')
}
其中問題有:
-
·postMessage
?傳遞消息的方式不適合現(xiàn)代編程模式,當(dāng)出現(xiàn)多個(gè)事件時(shí)就涉及分拆解析和解決耦合問題,因此需要改造 -
·新建?
worker
?需要單獨(dú)文件,因此項(xiàng)目?jī)?nèi)需要處理打包拆分邏輯,獨(dú)立出?worker
?文件 -
·worker
?內(nèi)可支持定義函數(shù),可通過importScript
?方式引入依賴文件,但是都獨(dú)立于主線程文件,依賴和函數(shù)的復(fù)用都需要改造 -
·多線程環(huán)境必然涉及同步運(yùn)行多個(gè)?
worker
,多?worker
?的啟動(dòng)、復(fù)用和管理都需要自行處理
看完這么多問題,有沒有感覺頭很大,一個(gè)設(shè)計(jì)這樣原始的 api,如何舒服的使用呢?
類庫(kù)調(diào)研
????????首先可以想到的就是借助成熟類庫(kù)的力量,下面表格是較為常見的幾款?worker
?類庫(kù),其中我們可能會(huì)關(guān)注的關(guān)鍵能力有:
-
·通信是否有包裝成更好用的方式,比如?
promise
?化或者?rpc
?化 -
·是否可以動(dòng)態(tài)創(chuàng)建函數(shù)——可以增加?
worker
?靈活性 -
·是否包含多?
worker
?的管理能力,也就是線程池 -
·考慮?
node
?的使用場(chǎng)景,是否可以跨端運(yùn)行
????????比較之下,workerpool[5]?勝出,它也是個(gè)年紀(jì)很大的庫(kù)了,最早的代碼提交在 6 年前,不過實(shí)踐下來(lái)沒有大問題,下文都會(huì)在使用它的基礎(chǔ)上繼續(xù)討論。
有類庫(kù)加持的 worker 現(xiàn)狀
????????通過使用?workerpool
,我們可以在主線程文件內(nèi)新建?worker
;它自動(dòng)處理多?worker
?的管理;可以執(zhí)行?worker
?內(nèi)定義好的函數(shù)?a
;可以動(dòng)態(tài)創(chuàng)建一個(gè)函數(shù)并傳入?yún)?shù),讓?worker
?來(lái)執(zhí)行。
//?index.js
import?workerpool?from?'workerpool'
const?pool?=?workerpool.pool('./worker.js')
//?執(zhí)行一個(gè)?worker?內(nèi)定義好的函數(shù)
pool.exec('a',?[1,?2]).then((res)?=>?{
??console.log(res)
})
//?執(zhí)行一個(gè)自定義函數(shù)
pool
??.exec(
????(x,?y)?=>?{
??????return?x?+?y
????},?//?自定義函數(shù)體
????[1,?2],?//?自定義函數(shù)參數(shù)
??)
??.then((res)?=>?{
????console.log(res)
??})
//?worker.js
importScripts('constant.js')
function?a()?{
??console.log('test')
}
但是這樣還不夠,為了可以舒適的寫代碼,我們需要進(jìn)一步改造。
向著舒適無(wú)感的 worker 編寫前進(jìn)
我們期望的目標(biāo)是:
-
·足夠靈活:可以隨意編寫函數(shù),今天我想計(jì)算
1+1
,明天我想計(jì)算1+2
,這些都可以動(dòng)態(tài)編寫,最好它可以直接寫在主線程我自己的文件里,不需要我跑到?worker
?文件里去改寫; -
·足夠強(qiáng)大:我可以使用公共依賴,比如?
lodash
?或者是項(xiàng)目里已經(jīng)定義好的某些公共函數(shù)。
????????考慮到?workerpool
?具備了動(dòng)態(tài)創(chuàng)建函數(shù)的能力,第一點(diǎn)已經(jīng)可以實(shí)現(xiàn);而第二點(diǎn)關(guān)于依賴的管理,則需要自行搭建,接下來(lái)介紹搭建步驟。
1. 抽取依賴,管理編譯和更新:
????????新增一個(gè)依賴管理文件worker-depts.js
,可按照路徑作為 key 名構(gòu)建一個(gè)聚合依賴對(duì)象,然后在?worker
?文件內(nèi)引入這份依賴
//?worker-depts.js
import?*?as?_?from?'lodash-es'
import?*?as?math?from?'../math'
const?workerDepts?=?{
??_,
??'util/math':?math,
}
export?default?workerDepts
//?worker.js
import?workerDepts?from?'../util/worker/worker-depts'
2. 定義公共調(diào)用函數(shù),引入所打包的依賴并串聯(lián)流程:
????worker
?內(nèi)定義一個(gè)公共調(diào)用函數(shù),注入 worker-depts 依賴,并注冊(cè)在?workerpool
?的方法內(nèi)
//?worker.js
import?workerDepts?from?'../util/worker/worker-depts'
function?runWithDepts(fn:?any,?...args:?any)?{
??var?f?=?new?Function('return?('?+?fn?+?').apply(null,?arguments);')
??return?f.apply(f,?[workerDepts].concat(args))
}
workerpool.worker({
??runWithDepts,
})
主線程文件內(nèi)定義相應(yīng)的調(diào)用方法,入?yún)⑹亲远x函數(shù)體和該函數(shù)的參數(shù)列表
//?index.js
import?workerpool?from?'workerpool'
export?async?function?workerDraw(fn,?...args)?{
??const?pool?=?workerpool.pool('./worker.js')
??return?pool.exec('runWithDepts',?[String(fn)].concat(args))
}
????????完成以上步驟,就可以在項(xiàng)目任意需要調(diào)用?worker
?的位置,像下面這樣。自定義函數(shù)內(nèi)容,引用所需依賴(已注入在函數(shù)第一個(gè)參數(shù)),進(jìn)行使用了。
????????這里我們引用了一個(gè)項(xiàng)目?jī)?nèi)的公共函數(shù)?fibonacci
,也引用了一個(gè)?lodash
?的?map
?方法,都可以在depts
?對(duì)象上取到
//?項(xiàng)目?jī)?nèi)需使用worker時(shí)
const?res?=?await?workerDraw(
??(depts,?m,?n)?=>?{
????const?{?map?}?=?depts['_']
????const?{?fibonacci?}?=?depts['util/math']
????return?map([m,?n],?(num)?=>?fibonacci(num))
??},
??input1,
??input2,
)
3. 優(yōu)化語(yǔ)法支持
????????沒有語(yǔ)法支持的依賴管理是很難用的,通過對(duì)?workerDraw
?進(jìn)行?ts
?語(yǔ)法包裝,可以實(shí)現(xiàn)在使用時(shí)的依賴提示:
import?workerpool?from?'workerpool'
import?type?TDepts?from?'./worker-depts'
export?async?function?workerDraw<T?extends?any[],?R>(fn:?(depts:?typeof?TDepts,?...args:?T)?=>?Promise<R>?|?R,?...args:?T)?{
??const?pool?=?workerpool.pool('./worker.js')
??return?pool.exec('runWithDepts',?[String(fn)].concat(args))
}
然后就可以在使用時(shí)獲取依賴提示:
4. 其他問題
????????新增了?worker
?以后,出現(xiàn)了?window
和?worker
?兩種運(yùn)行環(huán)境,如果你恰好和我一樣需要兼容?node
?端運(yùn)行,那么運(yùn)行環(huán)境就是三種,原本我們通常判斷 window 環(huán)境使用的也許是?typeof window === 'object'
這樣,現(xiàn)在不夠用了,這里可以改為 globalThis 對(duì)象,它是三套環(huán)境內(nèi)都存在的一個(gè)對(duì)象,通過判斷globalThis.constructor.name
的值,值分別是'Window' / 'DedicatedWorker'/ 'Object'
,從而實(shí)現(xiàn)環(huán)境的區(qū)分
總結(jié)
????????通過使用?workerpool
,添加依賴管理和構(gòu)建公共?worker
?調(diào)用函數(shù),我們實(shí)現(xiàn)了一套按需調(diào)用,靈活強(qiáng)大的?worker
?使用方式。
????????在京東羚瓏的程序化設(shè)計(jì)項(xiàng)目中,通過把 skia 圖形繪制部分逐步改造為?worker
內(nèi)調(diào)用,我們實(shí)現(xiàn)了整體服務(wù)耗時(shí)降低 75% 的效果,收益還是非常不錯(cuò)的。
????????文中涉及的代碼示例都已放在?github[6]?上,內(nèi)有?vite
?和?webpack
?兩個(gè)完整實(shí)現(xiàn)版本,感興趣的小伙伴可以 clone 下來(lái)參照著看~
參考資料
[1] Is postMessage slow:?https://dassur.ma/things/is-postmessage-slow/
[2] redux 挪到了 worker 內(nèi):?https://blog.axlight.com/posts/off-main-thread-react-redux-with-performance
[3] dom 挪到了 worker 內(nèi):?https://github.com/ampproject/worker-dom
[4] 可使用多核能力的框架:?https://github.com/neomjs/neo
[5] workerpool:?https://github.com/josdejong/workerpool
[6] github:?https://github.com/Silencesnow/worker-demo-2022
[7] MDN Web Workers API:?https://developer.mozilla.org/zh-CN/docs/Web/API/Web_Workers_API
[8] workerpool:?https://github.com/josdejong/workerpool
[9] 前端項(xiàng)目上 Web Worker 實(shí)踐:?https://www.youtube.com/watch?v=AEpG-3XXrjk文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-464918.html
[10] Web Worker 文獻(xiàn)綜述:?https://juejin.cn/post/6854573213297410062文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-464918.html
到了這里,關(guān)于2023 年的 Web Worker 項(xiàng)目實(shí)踐的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!