Web Woeker和Shared Worker的使用以及案例

2023-11-01 18:00:20

1、前言

最近做的專案出現了介面卡頓的問題,經過一番排查,發現是因為有個資料做了一些格式化和生成轉換,本來只有 1000 條資料,處理完之後變成了 N 萬條資料(業務需求),導致頁面渲染很慢,甚至會崩潰。於是就想著優化一下。初始化的時候不載入,等需要的時候,再使用 Web Worker 來處理資料,避免主執行緒卡頓。

2、介紹 Web Worker

在介紹之前,先說一下Web Worker是為什麼而誕生的。

因為 JavaScript 語言採用的是單執行緒模型,也就是說,所有任務只能在一個執行緒上完成,一次只能做一件事。前面的任務沒做完,後面的任務只能等著。隨著電腦計算能力的增強,尤其是多核 CPU 的出現,單執行緒帶來很大的不便,無法充分發揮計算機的計算能力。

Web Worker 的作用,就是為 JavaScript 創造多執行緒環境。它是 HTML5 標準的一部分,它賦予了開發者利用 JavaScript 操作多執行緒的能力。允許主執行緒建立 Worker 執行緒,將一些任務分配給 Worker 執行緒執行。在主執行緒執行的同時,Worker 執行緒在後臺執行,兩者互不干擾。等到 Worker 執行緒完成計算任務,再把結果返回給主執行緒。這樣的好處是,一些計算密集型或高延遲的任務,被 Worker 執行緒負擔了,主執行緒(通常負責 UI 互動)就會很流暢,不會被阻塞或拖慢。

3、使用須知及相容性

在使用 Worker 前,需要先了解一些規則和瀏覽器的相容性,避免出現一些問題。

3.1、使用須知

  1. 資源耗費:Worker 執行緒一旦新建成功就會始終執行,不會被主執行緒上的活動(比如使用者點選按鈕、提交表單)打斷。這樣有利於隨時響應主執行緒的通訊。但是也造成了 Worker 比較耗費資源,建議使用完畢就關閉。

  2. 同源限制:分配給 Worker 執行緒執行的指令碼檔案,必須與主執行緒的指令碼檔案同源。

  3. DOM 限制:Worker 執行緒所在的全域性物件是 self,它與主執行緒不一樣,無法讀取主執行緒所在網頁的 window,DOM,document,parent 等全域性物件,但可以讀取主執行緒的:navigator 和 location 物件。

  4. 指令碼限制:Web Worker 中可以使用 XMLHttpRequest 和 Axios 傳送請求。

  5. 通訊聯絡:Worker 執行緒和主執行緒不在同一個上下文環境,它們不能直接通訊,必須通過訊息完成。

  6. 檔案限制:Worker 執行緒中無法讀取本地檔案,即不能開啟本機的檔案系統(file://),它所載入的指令碼,必須來自網路。

3.2、相容性

瀏覽器 相容性 最低相容版本
Chrome 完全相容 4.0 (2008 年)
Firefox 完全相容 3.5 (2009 年)
Safari 完全相容 3.1 (2007 年)
Edge 完全相容 79 (2020 年)
IE 部分相容 10 (2012 年)
Opera 完全相容 10.5 (2010 年)

4、使用 Web Worker

直接使用 JavaScript 原生的 Worker()建構函式,它的引數如下:

引數 說明
path 有效的 js 指令碼的地址,必須遵守同源策略
options.type 可選。用以指定 worker 型別。該值可以是 classic 或 module,預設 classic
options.credentials 可選。指定 worker 憑證。該值可以是 omit, same-origin,或 include。如果未指定,或者 type 是 classic,將使用預設值 omit (不要求憑證)
options.name 可選。在 DedicatedWorkerGlobalScope 的情況下,用來表示 worker 的 scope 的一個 DOMString 值,主要用於偵錯目的。

4.1、建立 Web Worker

主執行緒:

const myWorker = new Worker('/worker.js')

// 接收訊息
myWorker.addEventListener('message', (e) => {
	console.log(e.data)
})

// 向 worker 執行緒傳送訊息
myWorker.postMessage('Greeting from Main.js')

4.2、與主執行緒通訊

worker 執行緒:

// 接收到訊息
self.addEventListener('message', (e) => {
	console.log(e.data)
})

// 一頓計算後 傳送訊息
const calculateDataFn = () => {
	self.postMessage('ok')
}

4.3、終止 Web Worker

兩個執行緒裡都可以操作,自由選擇。

  • 在主執行緒中操作:
// 建立worker
const myWorker = new Worker('/worker.js')
// 關閉worker
myWorker.terminate()
  • 在 worker 執行緒中操作:
self.close()

4.4、監聽錯誤資訊

Web Worker 提供了兩個事件監聽錯誤回撥,error 和 messageerror。

事件 描述
error 當 worker 內部出現錯誤時觸發
messageerror 當 message 事件接收到無法被反序列化的引數時觸發
  • 在主執行緒中操作:
// 建立worker
const myWorker = new Worker('/worker.js')

myWorker.addEventListener('error', (err) => {
	console.log(err.message)
})

myWorker.addEventListener('messageerror', (err) => {
	console.log(err.message)
})
  • 在 worker 執行緒:
self.addEventListener('error', (err) => {
	console.log(err.message)
})
self.addEventListener('messageerror', (err) => {
	console.log(err.message)
})

5、使用 Shared Worker

SharedWorker 允許多個頁面共用同一個後臺執行緒,從而實現更高效的資源利用和協同計算。如下,是一個例子,page1page2 共用一個後臺執行緒:

  • sharedWorker.js
/**
 * @description 所有連線這個worker的集合
 */
const portsList = []

/**
 * @description 連線成功回撥
 */
self.onconnect = (event) => {
	// 當前觸發連線的埠
	const port = event.ports[0]
	// 新增進去
	portsList.push(port)
	// 接收到訊息的回撥
	port.onmessage = (event) => {
		// 獲取傳遞的訊息
		const { message, value } = event.data
		// 計算
		let result = 0
		switch (message) {
			case 'add':
				result = value * 2
				break
			case 'multiply':
				result = value * value
				break
			default:
				result = value
		}
		// 給所有連線的目標傳送訊息
		portsList.forEach((port) => port.postMessage(`${message}結果是:${result}`))
	}
}
  • sharedWorkerHook.js
const sharedWorker = new SharedWorker(new URL('../../utils/webworker.js', import.meta.url), 'test')

export default sharedWorker
  • page1
<template>
  <div @click="sendMessage">點選1</div>
</template>

<script>
import sharedWorkerHook from './sharedWorkerHook'

export default {
  name: '',
  data() {
    return {}
  },
  computed: {},
  created() {},
  mounted() {
    sharedWorkerHook.port.start()
    // 接收SharedWorker返回的結果
    sharedWorkerHook.port.onmessage = event => {
      console.log(event.data)
    }
  },
  methods: {
    sendMessage() {
      sharedWorkerHook.port.postMessage({ message: 'add', value: 1 })
    }
  }
}
</script>
  • page2
<template>
  <div @click="sendMessage">點選2</div>
</template>

<script>
import sharedWorkerHook from './sharedWorkerHook'

export default {
  name: '',
  data() {
    return {}
  },
  computed: {},
  created() {},
  mounted() {
    sharedWorkerHook.port.start()
    // 接收SharedWorker返回的結果
    sharedWorkerHook.port.onmessage = event => {
      console.log(event.data)
    }
  },
  methods: {
    sendMessage() {
      sharedWorkerHook.port.postMessage({ message: 'multiply', value: 1 })
    }
  }
}
</script>

4.5、偵錯 Shared Worker

sharedWorker 中偵錯,使用 console 列印資訊,不會出現在主執行緒的的控制檯中,需要在 Chrome 瀏覽器位址列輸入 chrome://inspect/,進入偵錯面板才能看到,步驟如下:

  1. 在 Chrome 瀏覽器位址列輸入 chrome://inspect,並回車進入
  2. 左邊選單欄,點選 sharedWorker
  3. 右邊選單欄,點選 inspect,即可開啟偵錯面板

6、使用中的一些坑

在使用中,雖然查閱了一些檔案和部落格,但是還是出現了以下問題,記錄一下。

6.1、Web Woeker 中引入了其餘檔案

有一些場景,需要放到 worker 程序去處理的任務很複雜,就會引入其餘檔案,這時候,可以在worker執行緒中利用importScripts()方法載入我們需要的js檔案

importScripts('./utils.js')

如果引入的是ESModule模式,需要在初始化的時候,指定type的模式。

const worker = new Worker('/worker.js', { type: 'module' })

6.2、在 WebPack 或 Vite 中使用

在webpack和vite中使用,步驟如下:

6.2.1、webpack中使用

第一步:安裝外掛:worker-plugin

npm install worker-plugin -D

第二步:在vue.config.js的configureWebpack.plugins中設定

const WorkerPlugin = require('worker-plugin')

module.exports = {
  outputDir: 'dist',
  // 其餘設定......
  configureWebpack: {
    devServer: {
      open: false,
      host: 'localhost',
      // 其餘設定......
    },
    plugins: [
      // 其餘設定......
      new WorkerPlugin()
    ]
  }
}

第三步:使用

const webWorker = new Worker(new URL('../utils/worker.js', import.meta.url), {
  type: 'module'
})

5.2.2、vite中使用

第一步:安裝外掛:worker-plugin

npm install worker-plugin -D

第二步:在vite.config.js的plugins中設定

import worker from 'worker-plugin'

const viteConfig = defineConfig((mode: ConfigEnv) => {
	return {
		plugins: [vue(), worker()],
		server: {
			host: '0.0.0.0',
			port: 7001,
			open: true
		}
	}
})

export default viteConfig

第三步:使用

const webWorker = new Worker(new URL('../utils/worker.ts', import.meta.url), {
  type: 'module'
})

5.3、sharedWorker的引入問題

在使用sharedWorker的過程中,發現sharedWorker程序裡,始終只有一個範例,但是我確實在幾個頁面都初始化了同一個sharedWorker的js檔案,最後偵錯發現,原因是通過外掛引入之後名字變了,一個是xxxx0.js,一個是xxxx1.js,所以導致我每次初始化的時候,都不認為是同一個範例,所以訊息無法同步。

  • 解決辦法:如[使用 Shared Worker](#5-使用 Shared Worker)裡的例子一樣,專門用一個檔案new好這個sharedWorker,然後匯出,在需要的頁面匯入後再執行即可。

7、後語

在本文中,我們學習了Web Worker的作用和使用方法,以及如何在Vue中使用Web Worker,最後,我們還學習了Shared Worker的使用方法。

其實webworker家族裡還有一位更加強大的成員,那就是Service Worker。它可以做的東西很多,比如攔截全域性的fetch事件、快取靜態資源/離線快取用於首屏載入、後臺同步,訊息推播等等,奈何篇幅有限,下回再做講解。


本次分享就到這兒啦,我是鵬多多,如果您看了覺得有幫助,歡迎評論,關注,點贊,轉發,我們下次見~


PS:在本頁按F12,在console中輸入document.querySelectorAll('.diggit')[0].click(),有驚喜哦


公眾號

往期文章

個人主頁