- 相關(guān)推薦
有關(guān)深入理解JavaScript中的并行處理的介紹
前言
為什么說(shuō)多線(xiàn)程如此重要?這是個(gè)值得思考的問(wèn)題。一直以來(lái),派生線(xiàn)程以一種優(yōu)雅的方式實(shí)現了對同一個(gè)進(jìn)程中任務(wù)的劃分。操作系統負責分配每個(gè)線(xiàn)程的時(shí)間片,具有高優(yōu)先級并且任務(wù)繁重的線(xiàn)程將分配到更多的時(shí)間片,而低優(yōu)先級空閑的線(xiàn)程只能分到較少的時(shí)間片。
雖然多線(xiàn)程如此重要,但JavaScript卻并沒(méi)有多線(xiàn)程的能力。幸運的是,隨著(zhù) Web Worker 的普及,我們終于可以在后臺線(xiàn)程來(lái)處理資源密集型的計算了。而不好的方面是,目前制定的標準只適用于當前的生態(tài)系統,這有時(shí)候就比較尷尬了。如果你了解其他從一開(kāi)始就支持多線(xiàn)程的語(yǔ)言的話(huà),你可能會(huì )發(fā)現很多的限制,遠非僅僅是實(shí)例化一個(gè)新線(xiàn)程,然后你操控這個(gè)實(shí)例就能實(shí)現多線(xiàn)程。
這篇文章主要來(lái)介紹 Web Worker,包括什么時(shí)候使用,該怎么使用,它有什么奇怪的特性,會(huì )介紹在 Webpack 中如何使用它,還有可能遇到的一些坑。
一、Web Workers
Web Worker 可能是在 JavaScript 中唯一可以真正實(shí)現多線(xiàn)程的方法了。我們需要按照下面的方式創(chuàng )建 worker :
const worker = newWorker("worker.js");
上面就定義了一個(gè) Worker 實(shí)例,然后你可以通過(guò) postMessage 與 worker 通信,就像和 iFrame 通信一樣,只不過(guò)不存在跨域的問(wèn)題,不需要驗證跨域。
worker.postMessage(num);
在 worker 代碼中,你需要監聽(tīng)這些事件:
onmessage = (e) => { // e.data will contain the value passed};
這種方式是雙向的,所以你也可以從 worker 中 postMessage 給我們的主程序。
在 worker 代碼中:
postMessage(result);
在主程序中:
worker.onmessage = (e) => {}
這就是 worker 最基本的用法。
異常處理
在你的 worker 代碼中,有很多種方式來(lái)處理異常,比如你可以 catch 之后通過(guò) postMessage 傳遞,這樣可能需要多些一些代碼,但是確實(shí)最有效也最安全的。
另一種方式是用 onerror 事件,這種方式可以捕捉所有未處理的異常,并且交給調用方來(lái)決定如何處理。調用方式很簡(jiǎn)單:
worker.onerror = (e) => {};
為了調試方便,異常對象中還有一些額外的字段比如:filename,lineno,colno.
回收
將不需要的 worker 回收是非常重要的,worker 會(huì )生成真正的操作系統線(xiàn)程,如果你發(fā)現與很多 worker 線(xiàn)程同時(shí)運行,你可以通過(guò)很簡(jiǎn)單的殺掉瀏覽器進(jìn)程。
你有兩種方式殺掉 worker 進(jìn)程:在 worker 里和在 worker 外。我認為最好的處理 worker 生命周期的地方是在主頁(yè)面里,但這也要取決于你代碼的具體情況。
殺掉一個(gè) worker 實(shí)例,在外部可以直接調用 terminate()方法,這種方法可以立即殺掉它,釋放所有它正在使用的資源,如果它正在運行,也會(huì )立即終止。
如果你想要讓 worker 自己去管理它的生命周期,可以直接在 worker 代碼中調用stop()方法。
不管使用哪種方法,worker 都會(huì )停止,銷(xiāo)毀所有資源。
如果你想使用一種“一次性”的 worker,比如需要做一些復雜運算之后就不再使用了,也要確保在 onerror 事件中去銷(xiāo)毀它們,這樣有利于規避一些難以發(fā)現的問(wèn)題。
worker.onerror = (e) => { worker.terminate(); reject(e);};worker.onmessage = (e) => { worker.terminate(); resolve(e.data);}
二、行內 Workers
有些時(shí)候將 worker 代碼寫(xiě)到一個(gè)外部文件可能會(huì )使原本簡(jiǎn)單的問(wèn)題變得復雜,幸運的是,workers 也可以用一個(gè) Blob 來(lái)初始化。
寫(xiě)一個(gè)行內 worker ,參考如下代碼段:
// Put your worker code here
const code = URL.createObjectURL(new Blob([ document.getElementById("worker").textContent]));const worker = new Worker(code);
這樣你就創(chuàng )建了一個(gè)全局的 ObjectURL,但別忘了當不需要的時(shí)候要銷(xiāo)毀它:
worker.terminate();URL.revokeObjectURL(code);
三、Workers 嵌套
理論上,你可以嵌套使用 worker,就像在主線(xiàn)程中定義一個(gè) worker 一樣。這里有一個(gè)簡(jiǎn)單的 例子。但是不幸的是在 Chrome 中一直存在一個(gè) bug ,讓我們不能愉快的玩耍,或許以后這個(gè) bug 會(huì )修復,但是目前來(lái)說(shuō)還是沒(méi)有太多進(jìn)展,所以你最好不要使用。
數據傳遞
在 worker 數據傳遞的過(guò)程中有些需要注意的邊緣情況。你可以傳遞數值,字符串,數組,也可以傳遞序列化/反序列化的對象。然而,你卻不應該依賴(lài)序列化來(lái)保持數據結構,實(shí)際上,postMessage 用到了一種 數據克隆算法,它會(huì )生成一些額外的屬性比如 RegExps 和 Blobs 以及一些循環(huán)引用。
這就是說(shuō),你需要將你要傳遞的數據最小化。你不可以傳遞 functions ,即使是支持的類(lèi)型也會(huì )有一些限制,這些也很容易產(chǎn)生一些難以發(fā)現的 bug。如果你將你的 API 定義為只傳遞字符串,數值,數組和對象的話(huà),那你可能會(huì )避過(guò)這些問(wèn)題。
循環(huán)引用
如果你有一個(gè)很復雜的對象,那么里面很可能存在循環(huán)引用,這時(shí)如果你將它序列化成 JSON,你將會(huì )得到一個(gè) TypeError: Converting circular structure to JSON.
let a = {};let b = {a};a.b = b;JSON.stringify({a,b}); // Error
然而你可以在 postMessage 中放心的使用,從而你就可以在 worker 中使用。
Transferable objects
為了防止同時(shí)修改同一變量的場(chǎng)景,你傳遞給 postMessage 的所有變量都會(huì )復制一份,這樣確保了你多個(gè)線(xiàn)程不會(huì )修改同一個(gè)變量。但如果你想要傳一個(gè)非常大的數據的話(huà),你就會(huì )發(fā)現復制操作是很慢的。比如,如果你在做一些圖片相關(guān)的運算,你可能會(huì )傳遞整個(gè)圖片信息,就可能會(huì )遇到復制性能的瓶頸。
好在有 transferable object ,用 transfer 來(lái)代替 copy,比如ArrayBuffer 是transferable對象,而我們可以把任何類(lèi)型的對象放在 ArrayBuffer 中。
如果你 transfer 一個(gè)對象,之前擁有它的線(xiàn)程被鎖定權限,它確保了數據沒(méi)有復制之前,不會(huì )被同時(shí)修改。
這時(shí) postMessage 的代碼段就有點(diǎn)尷尬了:
const ab = new ArrayBuffer(100);console.log(ab.byteLength); // 100worker.postMessage(ab, [ab]);console.log(ab.byteLength); // 0
確保在 postMessage 中傳遞第二個(gè)參數,否則數據將會(huì )被復制。
const ab = new ArrayBuffer(100);console.log(ab.byteLength); // 100worker.postMessage(ab);console.log(ab.byteLength); // 100
四、Webpack
在 Webpack 中使用 Web worker 時(shí),你需要用 worker-loader。將它添加到 package.json 中的 devDependencies,然后運行 npm install,就可以了。
用到 worker 時(shí),只需要 require 它。
const workerCode = require("worker!./worker.js");...const worker = new workerCode();
這樣就初始化了 worker,然后就像上面講的一樣使用 worker。
如果需要使用行內 worker,你需要傳遞 inline 參數給 loader。
const workerCode = require("worker?inline!./worker.js");...const worker = new workerCode();
在 worker 中你也可以 import 模塊。
import fibonacci from "./fibonacci.js";...const result = fibonacci(num);
缺點(diǎn)
在 Webpack 中使用 worker 很簡(jiǎn)單,但是在使用時(shí)也有一些坑值得你注意。
首先,無(wú)法將代碼共用部分提取出來(lái)。如果你的 worker 中依賴(lài)一段共用代碼,你只能把代碼添加到 worker 中,不管其他地方是否也用到同樣的代碼。而且如果你多個(gè) worker 要用同樣的庫,你也需要在每個(gè) worker 中引入它們。
你可能會(huì )想如果你不用 worker-loader,然后用CommonsChunkPlugin指定一個(gè)新的入口,可能會(huì )解決這個(gè)問(wèn)題。但是不幸的是 worker 不像是瀏覽器 window ,一些 feature 不可用,所以一些代碼必須要引入。
同時(shí),用行內 worker 也不會(huì )解決問(wèn)題,共用的代碼依然會(huì )出現在多個(gè)地方。
第二點(diǎn)缺點(diǎn)是,行內 worker 可能會(huì )導致 ObjectURLs內存泄露.它們被創(chuàng )建出來(lái)以后就不會(huì )被釋放。這雖然不是一個(gè)大問(wèn)題,但是如果你有很多“一次性” worker 的話(huà),就會(huì )影響性能。
綜上所述,我個(gè)人建議是使用標準的 worker,注意在 worker 中引入了什么。還要注意使用緩存。
五、IFrames Web worker
IFrames Web worker 和 IFrame 很像,而且印象中 IFrame 也可以實(shí)現多線(xiàn)程。但是 IFrame 存在一些不是線(xiàn)程安全 API,比如 DOM 相關(guān),瀏覽器不能為他們生成新的線(xiàn)程,參考這里.
在 IFrame 跨域中,很多 API 它都沒(méi)有權限,也只能通過(guò) postMessage,就像 Web Worker 一樣。理論上,瀏覽器可以在不同的線(xiàn)程中運行 IFrame,也就可以用 IFrame 實(shí)現多線(xiàn)程。
但是實(shí)際并非如此,它還是單線(xiàn)程的,瀏覽器不會(huì )給它們額外的線(xiàn)程。
總結
Web Worker 解決了 JavaScript 一直以來(lái)的大難題,盡管它的語(yǔ)法有些奇怪而且有很多限制,但是它卻可以真真正正的解決問(wèn)題。從另外一方面來(lái)講,它也還是個(gè)嬰兒,某些方面還不是很成熟,不能讓我們完全依賴(lài),所以這個(gè)技術(shù)普及還有一段距離,目前適用場(chǎng)景也比較局限。所以說(shuō),如果你需要做多線(xiàn)程,不要再等待其他的什么技術(shù),學(xué)習 web worker 的邊緣問(wèn)題,避開(kāi)它的坑,你就可以很好的提高用戶(hù)體驗。以上就是這篇文章的全部?jì)热,希望對大家能有所幫助?/p>
【深入理解JavaScript中的并行處理的介紹】相關(guān)文章:
對javascript的理解03-29
淺談javascript中的單線(xiàn)程理解03-30
淺談如何深入學(xué)習Javascript中的this關(guān)鍵字04-02
理解JavaScript原型鏈教程03-30
javascript編程異常處理的方法03-31
javascript的閉包概念怎么理解03-29
在Java中執行JavaScript代碼04-01