这是我参与11月更文挑战的第4天,活动详情查看:2021最后一次更文挑战
我们经常说 JavaScript 是单线程,是指相对于 JAVA 等语言,JavaScript 不具备并行任务处理的特性。JS 具有一个主线程,所有的任务需要排队进行处理。主线程在执行前面的任务时,会阻塞后面的任务。
随着计算机处理能力的提高,这种单线程模型无法充分发挥多核 CPU 计算机的计算能力。为了让 JS 也能够有多线程能力,诞生了 Web Worker 。
现在我们可以在主线程中创建 worker 线程,将一些计算密集型或高延迟的任务分配给 worker 线程运行。worker 线程与主线程并行运行,互不干扰,这就避免了单线程任务阻塞的情况。
基本用法
首先需要注意一些限制:
- 同源限制:创建 worker 线程的时候需要分配一个 JS 文件,该文件必须是同源的,且不能是本地文件;
- 环境隔离:worker 线程所在的上下文环境与主线程不一样,无法读取网页的 DOM 对象,全局对象不再是 window,可以通过 this 或 self 访问。
- 通信受限:主线程和 worker 线程不能直接通信,通过
postMessage
方法进行消息传递。
在主线程中,调用 worker 构造函数,创建 worker 线程:
let worker = new Worker('work.js') // 必须传递一个 url,受同源限制
主线程通过调用 postMessage
方法,向 worker 线程传递数据,数据类型不限:
worker.postMessage('Hello World');
worker.postMessage({a:1, b:2});
主线程通过监听 onmessage
事件,接收子线程传递的信息:
worker.onmessage = function(event) {
console.log(event.data); // data 就是 worker 传递的数据
}
woker 线程内同样通过监听 onmessage
事件,接收主线程传递的数据,通过 postMessage
传递数据:
self.addEventListener('message', function(event) {
console.log(event.data); // data 为传递的数据
// do something...
self.postMessage('get✔');
})
错误处理:
// 主线程
worker.onerror(function (event) {
console.log([
'ERROR: Line ', e.lineno, ' in ', e.filename, ': ', e.message
].join(''));
});
// 子线程
self.onerror(function (event) {
console.log([
'ERROR: Line ', e.lineno, ' in ', e.filename, ': ', e.message
].join(''));
});
在 woker 线程执行完毕之后,需要关闭线程,释放资源:
// 主线程
worker.terminate();
// 子线程
self.close();
在 HTML 中使用 worker
worker 除了载入同源的 JS 脚本文件外,也可以是同一页面的代码。在页面中创建一段浏览器不解析的标签,转成 blob url 后就可以作为参数传递给 worker 构造函数了,例如:
<!-- 写一个浏览器不认识的 type,这段脚本就不会被解析执行 -->
<script id="worker" type="app/worker">
console.log(self);
self.onmessage = function(event) {
console.log('子线程收到消息:', event.data);
self.postMessage('get✔');
}
self.onerror = function (err) {
console.log('子线程异常:', err);
}
throw new Error('test error');
</script>
然后再将脚本内容转成 blob-url,创建 worker 线程运行:
<script>
let blob = new Blob([document.querySelector('#worker').textContent]);
let url = window.URL.createObjectURL(blob);
let worker = new Worker(url);
worker.onmessage = function(event) {
console.log('主线程收到消息:', event.data);
}
worker.onerror = function (err) {
console.log('主线程收到子线程异常:', err);
}
worker.postMessage('Hello World');
</script>
完整代码如下,试试看会输出什么:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Web Worker</title>
</head>
<body>
<script id="worker" type="app/worker">
console.log(self);
self.onmessage = function(event) {
console.log('子线程收到消息:', event.data);
self.postMessage('get✔');
}
self.onerror = function (err) {
console.log('子线程异常:', err);
}
throw new Error('test error');
</script>
<script>
let blob = new Blob([document.querySelector('#worker').textContent]);
let url = window.URL.createObjectURL(blob);
let worker = new Worker(url);
worker.onmessage = function(event) {
console.log('主线程收到消息:', event.data);
}
worker.onerror = function (err) {
console.log('主线程收到子线程异常:', err);
}
worker.postMessage('Hello World');
</script>
</body>
</html>
在 vue 项目中使用 worker
webpack | vite
如果项目使用的 webpack 4,需要安装 worker-loader
,参考文档说明。webpack 5 则不需要安装 worker-loader
,具体使用可以参考文档说明
vite 不需要安装插件,通过 ?worker
或 ?sharedworker
进行导入,默认导出一个自定义的 worker 构造器。具体使用可以参考文档说明。
演示
先使用 vite 创建了一个 vue 项目:
$ npm init vite@latest vue-worker --template vue
$ npm i
$ npm run dev
页面展示如下:
接下来我们利用 worker 实现功能:点击 count 按钮的时,计算出 count 对应的 斐波那契数,进行展示。
首先编写一个 worker.js 文件,进行斐波那契数的计算:
// /src/worker.js
/**
* 计算斐波那契数
* @param {number} n
* @returns {number} 返回第 n 个斐波那契数
*/
function fib(n) {
let arr = [0,1];
for (let i = 2; i <= n; i++) {
arr[i] = arr[i-1] + arr[i-2];
}
return arr[n];
}
self.onmessage = function (e) {
let n = e.data;
self.postMessage(fib(n));
self.close();
}
修改 HalloWorld.vue 文件,引入 worker,增加侦听器和 fibCount 属性,在页面展示:
import { ref, watch } from 'vue'
import myWorker from '../worker.js?worker' // 引入 worker 文件,通过 ?worker 标识
defineProps({
msg: String
})
const count = ref(0)
const fibCount = ref(0);
watch(count, (val) => { // 监听到 count 改变的时候,创建 worker 线程计算斐波那契数列。
let worker = new myWorker;
worker.postMessage(count.value);
worker.onmessage = (e) => {
console.log(e.data);
fibCount.value = e.data;
}
});
增加展示标签:
<button type="button" @click="count++">count is: {{ count }}</button>
++ <p>第 {{ count }} 个斐波那契数为 {{ fibCount }}</p>
是不是很简单,运行看看效果如何:
最后,我们再看看打包后的文件是怎么样的:
$ npm run build
生成 dist 文件夹的文件如下:
可以看到 worker.js 被打包成单独的一个文件,进入 index.e375bf05.js 文件搜索关键词,看到 worker 构造函数通过 url 进行载入:
总结
- 使用 worker 构造函数创建 worker 线程,需要传递一个同源脚本文件 URL;
- 使用
postMessage
、onmessage
方法事件进行消息传递; - 通过
onerror
事件处理异常,使用close
方法关闭线程; - worker 线程任务可以是 JS 脚本文件,也可以是内联脚本;
- 在项目中使用 worker,webpack 4 需要安装
worker-loader
,webpack 5 和 vite 原生支持。