瀏覽器執行環境是單線程的,一旦出現【主線程】耗時操作,就會造成瀏覽器卡死,用戶點擊沒響應等情況。
Web Worker
可以創建一個獨立于主線程運行的子線程。可以將一些【可能會阻塞主線程的操作】,丟在 Worker
里去單獨執行。
那為什么平時都沒有意識到主線程阻塞這個問題呢?
因為下大多數情況下,我們不太關心瀏覽器主線程是否會被阻塞,因為同步代碼執行一般都很快,慢的I/O、異步請求、定時器等操作,瀏覽器默認就幫我們異步操作了(變成宏、微任務了)。
比如:
- 異步請求
- 定時器
- 宏、微任務等。
阻塞主線程 - 一般不會出現
事實上,一般項目開發中,很難會用阻塞主線程的業務邏輯代碼出現。 但是為了驗證這個問題,這里可以模擬一個非常尷尬的場景。
// 創建一個 sleep 函數,模擬主線程阻塞的情況
function sleep (wait = 5000) {
let now = new Date()
while (new Date() - now < wait) { }
}
sleep() // 阻塞主線程
document.addEventListener.call(btn, "click", function () {
alert('點我有反應嗎?')
})
我們會在瀏覽器上放一個按鈕,并給按鈕綁定點擊事件。
但由于 sleep
耗時 5 秒鐘,在此過程中,主線程就阻塞了。
可以發現,由于主線程被阻塞,瀏覽器都無法正常渲染了,都出現了黑屏。 點擊按鈕也半天沒反應。
使用 Worker 解決主線程耗時造成的阻塞問題
可以將主線程一些耗時的【同步】操作,丟給 Worker
來處理,將耗時操作丟在子線程中,這樣主線程就不會被阻塞,就正常處理渲染,處理用戶點擊,異步回到等。
WebWorker 的作用,就是為了 JavaScript 創建多線程環境,允許主線程創建 Worker 子線程,將一些【可能會阻塞主線程】的任務分配給 worker 子線程去處理。兩者互不干擾。等子線程處理完成之后,在通過消息的機制把處理結果返回給主線程即可。充分利用多線程的優勢。
Worker 線程畢竟占據的一個操作系統的線程資源,在 Worker 的任務執行完畢之后,即可將其關閉,釋放操作系統線程資源。
Worker 使用說明和 API
使用說明
- 同源限制
分配給 worker 線程的腳本,必須和主線程腳本同源。(否則無法創建 worker,且雙方無法通信)
- DOM限制
Worker 工作在子線程,和主線程不太一樣。所以并無法操作 DOM \ BOM 等 API。(純數據處理)
- 全局對象限制
Worker 全局對象不是
window
,所以一些window
上的全局屬性和方法也無法訪問。(但可以訪問Navigator
和Location
接口)
- 通信限制
由于
Worker
單獨運行在一個子線程,所以和主線程通信使用發布、訂閱的消息機制完成。
- 腳本限制
可以在 Worker 中使用
XMLHttpRequest
來發送異步請求。
- 運行環境限制
Worker 不能運行在
file://
協議下。(不能直接右鍵打開)
API
Worker 的 API 十分簡單,和 iframe
通信 API 類似。
主線程
// 在主線程中,使用 `new Worker` 構造函數,來創建一個 `Worker` 實例。
const worker = new Worker('worker.js',{name: 'worker'})
// 主線程中,向子線程 worker 發送數據
worker.postMessage(data)
// 主線程中,監聽子線程 worker 回發的數據
worker.onMessage = (data) => {
console.log('來自子線程的數據',data)
}
// 主線程中,監聽 worker 錯誤
worker.onerror = (err) => {}
// 主線程關閉 worker 子線程
worker.close()
子線程 worker.js
//在 worker 線程中監聽主線程發送過來的數據
self.onmessage = (data) => {
console.log('主線程發送過來的數據',data)
}
// 在 worker 中,向主線程發送數據
self.postMessage(data)
// 在 worker 中監聽錯誤
self.onerror = (err) => {}
// 在 worker 中關閉自己
self.close()
利用 Worker 解決上述主線程阻塞問題
// 在瀏覽器主線程中..
// 定義異步事件,worker 限制耗時操作不會阻塞主線程
document.addEventListener.call(btn, "click", function () {
alert('點我有反應嗎?')
})
// 主線程創建 worker
const worker = new Worker('./worker.js', { name: 'my-worker' })
// 主線程給 worker 子線程發送數據
worker.postMessage({ id: 1, name: 'gqs' })
// 主線程注冊子線程的 postmessage 數據回調
worker.onmessage = ({ data }) => {
console.log('來自子線程 worker 的數據', data)
}
// 在 worker.js 子線程中
console.log(self.name)
self.onmessage = function ({ data }) {
console.log('接受到了來自主線程的數據:', data)
sleep(10000) // 耗時 10s
self.postMessage({ id: 1, name: 'gqs' })
}
self.onerror = function (err) {
// worker 線程發生了錯誤!
throw new Error(err)
}
// 模擬會阻塞主線程的耗時操作.
function sleep (wait = 5000) {
let now = new Date()
while (new Date() - now < wait) { }
}
我們將【會阻塞主線程】的耗時操作,丟在 worker 中去單獨處理后,在查看效果。
總結
一般情況下,你不會用到
Worker
,因為瀏覽器主線程一般很少會出現很復雜從而導致阻塞的操作。默認的耗時操作,都會瀏覽器默認處理為宏、微任務了。
當遇到了類似上述那種,在主線程同步環境下造成阻塞的問題,可以使用
worker
來創建一個子線程進行處理,提高用戶體驗。