日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

尝鲜 workerize 源码

發布時間:2025/3/20 编程问答 30 豆豆
生活随笔 收集整理的這篇文章主要介紹了 尝鲜 workerize 源码 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

寫在前面

最近正好在看web worker相關的東西,今天無意中就看到了github一周最熱項目的推送中,有這么一個項目workerize,repo里的文檔的描述如下:

Moves a module into a Web Worker, automatically reflecting exported functions as asynchronous proxies.

例子

關于README很簡單,包含一個類似hello world的例子就沒其他什么了。但是從例子本身可以看出這個庫要解決的問題,是想通過模塊化的方式編寫運行在web worker中的腳本,因為通常情況下,web worker每加載一個腳本文件是需要通過一個符合同源策略的URL的,這樣會對服務端發送一個額外的請求。同時對于web worker本身加載的js文件的執行環境,與主線程是隔離的(這也是它在進行復雜運算時不會阻塞主線程的原因),與主線程的通訊靠postMessageapi和onmessage回調事件來通訊,這樣我們在編寫一些通信代碼時,需要同時在兩個不同的環境中分別編寫發送消息和接受消息的邏輯,比較繁瑣,同時這些代碼也不能以模塊化的形式存在。

如果存在一種方式,我們可以以模塊化的方式來編寫代碼,注入web worker,之后還能通過類似Promsie機制來處理等異步,那便是極好的。

先來看看例子:

import workerize from 'workerize'let worker1 = workerize(`export function add(a, b) {let start = Date.now();while (Date.now()-start < 500);return a + b;}export default function minus(a, b){let start = Date.now();while (Date.now()-start < 500);return a - b} `)let worker2 = workerize(function (m) {m.add = function (a, b) {let start = Date.now()while (Date.now() - start < 500);return a + b} });(async () => {console.log('1 + 2 = ', await worker1.add(1, 2))console.log('3 + 9 = ', await worker2.call('add', [3, 9])) })()

worker1和worker2是兩種不同的使用方式,一種是以字符串的形式聲明模塊,一種以函數的形式聲明模塊。但是無論哪種,最后的結果都是一樣的,我們可以通過worker實例顯示的調用我們想要調用的方法,每個方法的調用結果均是一個Promise,因此它還可以完美的適配async/await語法。

源碼

那么問題來了,這種模塊的加載機制和調用方式是怎樣實現的呢?我在運行demo代碼的時候心中也默默想到,我去,看了好幾天的web worker原來還能這么玩,所以一定要研究研究它的源碼和它的實現原理。

打開源代碼才發現其實并沒有多少代碼,官文文檔也通過一句話強調了這一點:

Just 900 bytes of gzipped ES3

所以對其中主要的兩點進行簡單說明:

  • 如何實現按內容模塊化加載腳本而不是通過URL
  • 如何通過Promise來代理主線程與worker線程的通訊過程

使用Blob動態生成加載腳本資源

let blob = new Blob([code], {type: 'application/javascript'}),url = URL.createObjectURL(blob),worker = new Worker(url)

這其實不是什么新鮮的東西,就是將代碼的內容轉化為Blob對象,之后再通過URL.createObjectURL將Blob對象轉化為URL的形式,之后再用worker加載它,僅此而已。但是這里的問題是,這個code是哪里從哪里來的呢?

將加載代碼模塊化

在加載代碼之前,還有重要的一步,就是需要將加載的代碼轉變為模塊,模板本身只對外暴露統一的接口,這樣不論對于主線程還是worker線程,就有了統一的約束條件。源碼中作者把上一步中的code轉化為了類似commonjs的形式,主要涉及的代碼有:

let exportsObjName = `__EXPORTS_${Math.random().toString().substring(2)}__`if (typeof code === 'function') code = `(${toCode(code)})(${exportsObjName})`code = toCjs(code, exportsObjName, exports)code += `\n(${toCode(setup)})(self, ${exportsObjName}, {})`

和toCjs方法

function toCjs (code, exportsObjName, exports) {exportsObjName = exportsObjName || 'exports'exports = exports || {}code = code.replace(/^(\s*)export\s+default\s+/m, (s, before) => {exports.default = truereturn `${before}${exportsObjName}.default = `})code = code.replace(/^(\s*)export\s+(function|const|let|var)(\s+)([a-zA-Z$_][a-zA-Z0-9$_]*)/m, (s, before, type, ws, name) => {exports[name] = truereturn `${before}${exportsObjName}.${name} = ${type}${ws}${name}`})return `var ${exportsObjName} = {};\n${code}\n${exportsObjName};` }

關于toCjs方法,如果你的正則知識比較扎實的話,可以發現,它做了一件事,就是將字符串類型的code中的所有導出方法的聲明,使用commonjs的導出語法替換掉(中間會涉及一些具體的語法規則),如下:

// 如果 exportsObjName 使用默認值 exports, ...代表省略代碼 export function foo(){ ... } => exports.foo = function foo(){ ... } export default ... => exports.default = ...

如果code是函數類型,則首先使用toCode函數將code轉化為string類型,之后再將它轉化為IIFE的形式,如下

// 如果 exportsObjName 使用默認值 exports, ...代表省略代碼 // 傳入的code是如下形式: function( m ){ ... } // 轉化為 (function( m ){... })(exports)

這里的exportsObjName代表模塊的名字,默認值是exports(聯想commonjs),不過這里會在一開始就隨機生成一個模塊名字,生成代碼如下:

let exportsObjName = `__EXPORTS_${Math.random().toString().substring(2)}__`

這樣只有我們按照約定的語法來編寫web worker加載的代碼,它便會加載了一個符合同樣約定的commonjs模塊。

使用 Promise 來做異步代理

經過上面兩步,web worker加載到了模塊化的代碼,但是worker線程與主線程進行通訊則是仍然需要通過postMessage方法和onmessage回調事件來進行,如果無法優雅地處理這里的異步邏輯,那么之前所做的工作其實意義并不大。

workerize針對這里的異步邏輯,設計了一個簡單的rpc協議(文檔中將這個稱作a tiny, purpose-built RPC),先來看一下源碼中的setup函數:

function setup (ctx, rpcMethods, callbacks) {ctx.addEventListener('message', ({ data }) => {// 只捕獲滿足條件的數據對象if (data.type === 'RPC') {// 獲取數據對象中的 id 屬性let id = data.idif (id != null) {// 如果數據對象中存在非空 method 屬性,則證明是主線程發送的消息if (data.method) {// 獲取所要調用的方法實例let method = rpcMethods[data.method]if (method == null) {// 如果所調用的方法實例不存在,則發送方法不存在的消息ctx.postMessage({ type: 'RPC', id, error: 'NO_SUCH_METHOD' })} else {// 如果方法存在,則調用它,并將調用結果按不同的類型發送Promise.resolve().then(() => method.apply(null, data.params)).then(result => { ctx.postMessage({ type: 'RPC', id, result }) }).catch(error => { ctx.postMessage({ type: 'RPC', id, error }) })}// 如果 method 屬性為空,則證明是 worker 線程發送的消息} else {// 獲取每個消息所對應的處于pending狀態的Promise實例let callback = callbacks[id]if (callback == null) throw Error(`Unknown callback ${id}`)delete callbacks[id]// 按消息的類型將Promise轉化為resolve狀態或reject狀態。if (data.error) callback.reject(Error(data.error))else callback.resolve(data.result)}}}})}

根據注釋我們可以知道,這里的setup函數包含了rpc協議的解析規則,因此主線程和worker線程對會調用該方法來注冊安裝這個rpc協議,具體的代碼如下:

  • 主線程: setup(worker, worker.rpcMethods, callbacks)
  • worker線程: code += `\n(${toCode(setup)})(self, ${exportsObjName}, {})

這兩處代碼都是在各自的作用域中,將rpc協議與當前加載的模塊綁定起來,只不過主進程所傳callbacks是有意義的,而worker則使用一個空對象代替。

注冊調用邏輯

在擁有了rpc協議的基礎上,只需要實現調用邏輯即可,代碼如下:

worker.call = (method, params) => new Promise((resolve, reject) => {let id = `rpc${++counter}`callbacks[id] = { method, resolve, reject }worker.postMessage({ type: 'RPC', id, method, params }) })

這個call方法,每次會將一次方法的調用,轉化為一個pending狀態的Promise實例,并存在callbacks變量中,同時向worker線程發送一個格式為調用方法數據格式的消息。

for (let i in exports) {if (exports.hasOwnProperty(i) && !(i in worker)) {worker[i] = (...args) => worker.call(i, args)} }

同時在初始化的過程中,會將主線程加載的模塊中的每個方法,都綁定一個快捷方法,其方法名與模塊中的函數聲明保持一致,內部則使用worker.call來完成調用邏輯。

最后

關于這個庫本身,還存在一些可以探討的問題,比如:

  • 是否支持依賴解析機制
  • 如果引入外部依賴模塊
  • 針對消息是否需要按隊列進行處理

關于前兩點,似乎作者有一個相同的項目,叫做workerize-loader,可以解決,關于第三點,作者在代碼中增加了todo,表示實現消息隊列機制可能沒有必要,因為當前的通訊基于postMessage,本身的結果已經是有序狀態的了。

關于源碼本身的分析大概就這樣了,希望可以拋磚引玉,如有錯誤,還望指正。

總結

以上是生活随笔為你收集整理的尝鲜 workerize 源码的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。