摘要:同時在初始化的過程中,會將主線程加載的模塊中的每個方法,都綁定一個快捷方法,其方法名與模塊中的函數(shù)聲明保持一致,內(nèi)部則使用來完成調(diào)用邏輯。
寫在前面
最近正好在看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的,這樣會對服務端發(fā)送一個額外的請求。同時對于web worker本身加載的js文件的執(zhí)行環(huán)境,與主線程是隔離的(這也是它在進行復雜運算時不會阻塞主線程的原因),與主線程的通訊靠postMessageapi和onmessage回調(diào)事件來通訊,這樣我們在編寫一些通信代碼時,需要同時在兩個不同的環(huán)境中分別編寫發(fā)送消息和接受消息的邏輯,比較繁瑣,同時這些代碼也不能以模塊化的形式存在。
如果存在一種方式,我們可以以模塊化的方式來編寫代碼,注入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是兩種不同的使用方式,一種是以字符串的形式聲明模塊,一種以函數(shù)的形式聲明模塊。但是無論哪種,最后的結果都是一樣的,我們可以通過worker實例顯示的調(diào)用我們想要調(diào)用的方法,每個方法的調(diào)用結果均是一個Promise,因此它還可以完美的適配async/await語法。
源碼那么問題來了,這種模塊的加載機制和調(diào)用方式是怎樣實現(xiàn)的呢?我在運行demo代碼的時候心中也默默想到,我去,看了好幾天的web worker原來還能這么玩,所以一定要研究研究它的源碼和它的實現(xiàn)原理。
打開源代碼才發(fā)現(xiàn)其實并沒有多少代碼,官文文檔也通過一句話強調(diào)了這一點:
Just 900 bytes of gzipped ES3
所以對其中主要的兩點進行簡單說明:
如何實現(xiàn)按內(nèi)容模塊化加載腳本而不是通過URL
如何通過Promise來代理主線程與worker線程的通訊過程
使用Blob動態(tài)生成加載腳本資源let blob = new Blob([code], { type: "application/javascript" }), url = URL.createObjectURL(blob), worker = new Worker(url)
這其實不是什么新鮮的東西,就是將代碼的內(nèi)容轉化為Blob對象,之后再通過URL.createObjectURL將Blob對象轉化為URL的形式,之后再用worker加載它,僅此而已。但是這里的問題是,這個code是哪里從哪里來的呢?
將加載代碼模塊化在加載代碼之前,還有重要的一步,就是需要將加載的代碼轉變?yōu)槟K,模板本身只對外暴露統(tǒng)一的接口,這樣不論對于主線程還是worker線程,就有了統(tǒng)一的約束條件。源碼中作者把上一步中的code轉化為了類似commonjs的形式,主要涉及的代碼有:
let exportsObjName = `__EXPORTS_${Math.random().toString().substring(2)}__` if (typeof code === "function") code = `(${toCode(code)})(${exportsObjName})` code = toCjs(code, exportsObjName, exports) code += ` (${toCode(setup)})(self, ${exportsObjName}, {})`
和toCjs方法
function toCjs (code, exportsObjName, exports) { exportsObjName = exportsObjName || "exports" exports = exports || {} code = code.replace(/^(s*)exports+defaults+/m, (s, before) => { exports.default = true return `${before}${exportsObjName}.default = ` }) code = code.replace(/^(s*)exports+(function|const|let|var)(s+)([a-zA-Z$_][a-zA-Z0-9$_]*)/m, (s, before, type, ws, name) => { exports[name] = true return `${before}${exportsObjName}.${name} = ${type}${ws}${name}` }) return `var ${exportsObjName} = {}; ${code} ${exportsObjName};` }
關于toCjs方法,如果你的正則知識比較扎實的話,可以發(fā)現(xiàn),它做了一件事,就是將字符串類型的code中的所有導出方法的聲明,使用commonjs的導出語法替換掉(中間會涉及一些具體的語法規(guī)則),如下:
// 如果 exportsObjName 使用默認值 exports, ...代表省略代碼 export function foo(){ ... } => exports.foo = function foo(){ ... } export default ... => exports.default = ...
如果code是函數(shù)類型,則首先使用toCode函數(shù)將code轉化為string類型,之后再將它轉化為IIFE的形式,如下
// 如果 exportsObjName 使用默認值 exports, ...代表省略代碼 // 傳入的code是如下形式: function( m ){ ... } // 轉化為 (function( m ){ ... })(exports)
這里的exportsObjName代表模塊的名字,默認值是exports(聯(lián)想commonjs),不過這里會在一開始就隨機生成一個模塊名字,生成代碼如下:
let exportsObjName = `__EXPORTS_${Math.random().toString().substring(2)}__`
這樣只有我們按照約定的語法來編寫web worker加載的代碼,它便會加載了一個符合同樣約定的commonjs模塊。
使用 Promise 來做異步代理經(jīng)過上面兩步,web worker加載到了模塊化的代碼,但是worker線程與主線程進行通訊則是仍然需要通過postMessage方法和onmessage回調(diào)事件來進行,如果無法優(yōu)雅地處理這里的異步邏輯,那么之前所做的工作其實意義并不大。
workerize針對這里的異步邏輯,設計了一個簡單的rpc協(xié)議(文檔中將這個稱作a tiny, purpose-built RPC),先來看一下源碼中的setup函數(shù):
function setup (ctx, rpcMethods, callbacks) { ctx.addEventListener("message", ({ data }) => { // 只捕獲滿足條件的數(shù)據(jù)對象 if (data.type === "RPC") { // 獲取數(shù)據(jù)對象中的 id 屬性 let id = data.id if (id != null) { // 如果數(shù)據(jù)對象中存在非空 method 屬性,則證明是主線程發(fā)送的消息 if (data.method) { // 獲取所要調(diào)用的方法實例 let method = rpcMethods[data.method] if (method == null) { // 如果所調(diào)用的方法實例不存在,則發(fā)送方法不存在的消息 ctx.postMessage({ type: "RPC", id, error: "NO_SUCH_METHOD" }) } else { // 如果方法存在,則調(diào)用它,并將調(diào)用結果按不同的類型發(fā)送 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 線程發(fā)送的消息 } else { // 獲取每個消息所對應的處于pending狀態(tài)的Promise實例 let callback = callbacks[id] if (callback == null) throw Error(`Unknown callback ${id}`) delete callbacks[id] // 按消息的類型將Promise轉化為resolve狀態(tài)或reject狀態(tài)。 if (data.error) callback.reject(Error(data.error)) else callback.resolve(data.result) } } } }) }
根據(jù)注釋我們可以知道,這里的setup函數(shù)包含了rpc協(xié)議的解析規(guī)則,因此主線程和worker線程對會調(diào)用該方法來注冊安裝這個rpc協(xié)議,具體的代碼如下:
主線程: setup(worker, worker.rpcMethods, callbacks)
worker線程: code += ` (${toCode(setup)})(self, ${exportsObjName}, {})
這兩處代碼都是在各自的作用域中,將rpc協(xié)議與當前加載的模塊綁定起來,只不過主進程所傳callbacks是有意義的,而worker則使用一個空對象代替。
注冊調(diào)用邏輯在擁有了rpc協(xié)議的基礎上,只需要實現(xiàn)調(diào)用邏輯即可,代碼如下:
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方法,每次會將一次方法的調(diào)用,轉化為一個pending狀態(tài)的Promise實例,并存在callbacks變量中,同時向worker線程發(fā)送一個格式為調(diào)用方法數(shù)據(jù)格式的消息。
for (let i in exports) { if (exports.hasOwnProperty(i) && !(i in worker)) { worker[i] = (...args) => worker.call(i, args) } }
同時在初始化的過程中,會將主線程加載的模塊中的每個方法,都綁定一個快捷方法,其方法名與模塊中的函數(shù)聲明保持一致,內(nèi)部則使用worker.call來完成調(diào)用邏輯。
最后關于這個庫本身,還存在一些可以探討的問題,比如:
是否支持依賴解析機制
如果引入外部依賴模塊
針對消息是否需要按隊列進行處理
關于前兩點,似乎作者有一個相同的項目,叫做workerize-loader,可以解決,關于第三點,作者在代碼中增加了todo,表示實現(xiàn)消息隊列機制可能沒有必要,因為當前的通訊基于postMessage,本身的結果已經(jīng)是有序狀態(tài)的了。
關于源碼本身的分析大概就這樣了,希望可以拋磚引玉,如有錯誤,還望指正。
文章版權歸作者所有,未經(jīng)允許請勿轉載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/92514.html
摘要:盡管等待了多年,但是最終還是發(fā)布了正式版本與上一個版本相比未有重大變化,主要著眼于部分錯誤修復與提升。能夠將異步函數(shù)移入獨立線程中,可以看做函數(shù)的單函數(shù)簡化版。不過需要注意的是,僅支持純函數(shù),其會在獨立的作用域中運行這些函數(shù)。 showImg(https://segmentfault.com/img/remote/1460000013038757); 前端每周清單專注前端領域內(nèi)容,以對...
閱讀 2572·2021-09-23 11:21
閱讀 1882·2021-09-22 15:15
閱讀 970·2021-09-10 11:27
閱讀 3440·2019-08-30 15:54
閱讀 651·2019-08-30 15:52
閱讀 1335·2019-08-30 15:44
閱讀 2349·2019-08-29 15:06
閱讀 2972·2019-08-28 18:21