前言
我們都知道JavaScript在瀏覽器中執(zhí)行采用的是單線程模型酝陈,也就是說,在同一時(shí)間下所有的任務(wù)都只能在一個(gè)線程上完成,只有上一件任務(wù)完成才能開始下一件任務(wù)杭措。如果javascript的代碼計(jì)算量太大辽社,執(zhí)行會(huì)耗費(fèi)很長的時(shí)間伟墙,會(huì)影響了其他任務(wù)的執(zhí)行,嚴(yán)重的還可能阻塞UI線程的渲染滴铅,導(dǎo)致頁面出現(xiàn)卡頓等情況戳葵。而且現(xiàn)在很多的cpu都是多核的,單線程執(zhí)行會(huì)浪費(fèi)很多cpu的性能汉匙。所以瀏覽器廠商提供的web worker
的接口譬淳,就是為了提供讓javascript
代碼運(yùn)行在多個(gè)線程的環(huán)境。web worker
的工作線程和主線程是分開的盹兢,兩者互不干擾邻梆,相互間通過事件接口進(jìn)行通信。不過web worker
提供的接口太原始了绎秒,不是很方便我們使用浦妄,每次實(shí)例化worker
之后都要預(yù)定義單獨(dú)的javascript
腳本文件,而且還要單獨(dú)維護(hù)一套通信的方法见芹。
所以我們得想一個(gè)辦法讓worker
用起來優(yōu)雅順手一點(diǎn)剂娄,最好是提供一個(gè)接口可以像函數(shù)一樣調(diào)用,比如像這樣:
const work = someWorker()
work.add(function count(n){
return n + 1
})
work.count(1).then(res => {
console.log(res) // 2
})
這樣看上去是不是比較直觀一點(diǎn)玄呛,配合async
和await
用起來就非常優(yōu)雅了阅懦,完全屏蔽了js主線程和worker
之間通信的細(xì)節(jié)。
實(shí)現(xiàn)方法
其實(shí)具體通信的解決方法很簡單徘铝,我們只需要在函數(shù)調(diào)用的時(shí)候耳胎,postMessage
要調(diào)用的函數(shù)和參數(shù)到worker
里面去惯吕,再監(jiān)聽worker
返回的結(jié)果。worker
內(nèi)部也是一樣的道理怕午,監(jiān)聽主線程傳過來的消息废登,再執(zhí)行相應(yīng)的函數(shù),用postMessage
返回執(zhí)行結(jié)果郁惜。
// js主線程
function invoke (method = '', params = []) {
const promise = new Promise((resolve, reject)=> {
worker.onmessage = (e) => {
resolve(JSON.parse(e.data))
}
worker.onerror = (e) => {
reject(e)
}
})
worker.postMessage(JSON.stringify({
method,
params
}))
return promise
}
// worker
self.onmessage = (e) => {
const {method, params} = JSON.parse(e).data
const result = self[method].apply(null, params)
postMessage(JSON.stringify(result))
}
// self是worker全局環(huán)境的引用堡距,和window差不多
// 所以調(diào)用self的方法就是調(diào)用全局環(huán)境下注冊的方法
這樣我們就實(shí)現(xiàn)了一個(gè)簡單的worker
通信模型,只需要在傳入worker
的js腳本中提前定義好函數(shù)兆蕉,就可以在主線程通過invoke
調(diào)用函數(shù)了羽戒。但這和我們的想法還是有點(diǎn)不一樣,我們的模型的可以動(dòng)態(tài)地往worker中添加函數(shù)虎韵,而且函數(shù)可以定義在主線程中易稠,這樣可以獲得更好的靈活性和可維護(hù)性。
那么問題來了劝术,實(shí)例化worker
需要傳入js文件的地址缩多,而且這個(gè)地址不能是file://
開頭的,意味著不能訪問本地的文件养晋,所以worker的腳本必須加載至網(wǎng)絡(luò)衬吆。那么有沒有一種好的方法可以動(dòng)態(tài)生成js代碼片段,而且能夠包裝成worker
可以接受的類型呢绳泉?
其實(shí)是有的逊抡,瀏覽器廠商提供了一個(gè)URL
的對象,這個(gè)對象有一個(gè)create?ObjectURL
方法零酪,這個(gè)方法可以接受一個(gè)二進(jìn)制對象生成URL冒嫡,所以我們還需要Blob
類來生成二進(jìn)制數(shù)據(jù),我們的問題就可以完美解決了四苇。
let funcStr = ''
// 我們把函數(shù)名和函數(shù)引用以key-value的方式用Map儲(chǔ)存起來
for (const [name, func] of methods.entries()) {
let str = ''
if (isArrowFunc(func)) {
str = `;var ${name} = ${Function.prototype.toString.call(func)}`
} else {
str = `;${Function.prototype.toString.call(func)}`
}
funcStr += str
}
const code = `${code};\n${worker_scheduler}`
const url = URL.createObjectURL(new Blob([code]))
const worker = new Worker(url)
其實(shí)原理也很簡單孝凌,我們通過Function.toString
這個(gè)方法得到函數(shù)的定義,相當(dāng)于把函數(shù)定義復(fù)制到了worker
腳本月腋。這里需要提醒一下的是蟀架,es6箭頭函數(shù)的函數(shù)定義和普通的函數(shù)有一定的區(qū)別,我們需要分別處理榆骚。
const fn1 = () => {}
function fn2 () {}
Function.prototype.toString.call(fn1)
// () => {}
Function.prototype.toString.call(fn2)
// function fn2 () {}
我們可以看到箭頭函數(shù)的定義沒有定義的名字片拍,不過我們可以通過Function.name
獲取到函數(shù)定義名。
fn1.name // "fn1"
fn2.name // "fn2"
接下來的東西都很簡單了妓肢,我們自己在內(nèi)部維護(hù)這樣的一套機(jī)制捌省,只需要對外暴露add
和invoke
兩個(gè)接口就可以讓主線程定義的函數(shù)跑在worker
當(dāng)中了。
所以我順手實(shí)現(xiàn)了一個(gè)庫funcwork碉钠,內(nèi)部實(shí)現(xiàn)代碼只要一百多行纲缓,對外暴露了3個(gè)方法卷拘,用起來很方便。
import funcwork form 'funcwork'
const { add, invoke, terminate } = funcwork()
function sayName (name) {
return `Hello ${name}!`
}
const sayHi () {
return 'Hi!'
}
async function requestInfo (url, id) {
return fetch(url, {id})
}
add(sayName, sayHi)
await invoke('sayName', ['naeco']) // Hello naeco!
await invoke('sayHi') // Hi!
await invoke('requestInfo', ['api/getUserInfo', 'xxx123456']) // user info...
// 不用的時(shí)候記得銷毀
terminate()
大家覺得不錯(cuò)的可以順手給個(gè)star??????
后續(xù)
其實(shí)web worker這個(gè)東西出現(xiàn)了也挺久了色徘,現(xiàn)在瀏覽器支持度已經(jīng)很不錯(cuò)了恭金,但是我發(fā)現(xiàn)實(shí)際項(xiàng)目還是很少人用到操禀。個(gè)人認(rèn)為主要原因有兩個(gè):
- 接口不友好
- 使用場景有限
針對第一點(diǎn)我們可以自己進(jìn)行封裝褂策,可以讓web worker
用起來像promise
一樣順手。第二點(diǎn)要看我們具體的業(yè)務(wù)場景了颓屑,一些計(jì)算量比較大的工作可以嘗試交給web worker
斤寂,比如canvas
和圖片的計(jì)算,服務(wù)器輪詢和上傳文件等等場景揪惦。