前言
很多同学在学习一些技术时,并没有理解这些技术所能解决的问题,没有这些技术会导致怎样的问题。结果在学了之后,只学了一个皮毛,学了有哪些 API,在项目中又难以去运用。本篇文章将从问题出发,探讨 WebWorker 是怎么来应用的。
概念
首先,我们必须知道的是 JavaScript 是单线程模型,即使通过事件循环、Web Workers、多进程/多线程浏览器架构以及异步API,让 JavaScript能够以一种看起来像是并行的方式执行代码,但其本质依然是单线程模型。
为了更加深刻的理解单线程模型,我们以如下代码为例:
javascript
<script>
function fib(n) {
if (n < 2) {
return n;
}
return fib(n - 1) + fib(n - 2);
}
fib(45);
const el = document.createElement('div');
el.innerHTML = 'Hello Web Worker';
document.documentElement.appendChild(el);
</script>
当我们刷新网页时,浏览器经历了很长的时间后,才停止旋转。这是因为页面中有JavaScript代码在执行且耗时过长,阻塞UI更新。
为了解决这个问题,出现了 Web Worker,为创造了多线程环境,允许主线程创建 Worker 线程,将一些任务分配给后者运行。在主线程运行的时候,Worker 线程也在运行。
javascript
<script>
var worker = new Worker('fib_worker.js');
const el = document.createElement('div');
el.innerHTML = 'Hello Web Worker';
document.documentElement.appendChild(el);
</script>
javascript
// fib_worker.js
function fib(n) {
if (n < 2) {
return n;
}
return fib(n - 1) + fib(n - 2);
}
const result = fib(45);
console.log('result is ', result);
可以看到,当刷新浏览器时,浏览器渲染按钮立即结束,且过了很长时间控制台输出 fib(45) 的结果,可以看到 worker 线程并不会阻塞主线程。通过这个例子,想必你对 WebWorker 基本的使用有了一定的认识,后面我们将举出更多的例子去理解 WebWorker。
基本用法
Worker 创建
javascript
// worker.js 文件对应的是所要执行的任务
var worker = new Worker('worker.js');
Worker 通信
主线程与 Worker 之间通信采用的是消息传递机制,具体如下:
javascript
// 主线程
// postMessage 发送消息
worker.postMessage('Main Hello World');
// 监听消息
work.onmessage = function (event) {
console.log('Main Received message ' + event.data);
}
// work 内部出错时
work.onerror = function (error) { }
javascript
// worker 线程
self.postMessage('Worker Hello World');
self.onmessage = function (event) {
console.log('Worker Received message ' + event.data);
}
self 是一个全局对象,它代表了 Worker 线程自身的环境。在 Worker 文件中,self 取代了在普通浏览器环境中全局可用的 window 对象。
Worker 销毁
worker 执行完任务后,通过销毁可以释放资源。
javascript
// 主线程
worker.terminate()
javascript
// worker 线程
self.close()
-
worker.terminate() 是在主线程中调用的方法,用于立即终止 Worker 线程。当你调用 terminate() 时,Worker 线程会被强制停止,它不会有机会完成当前正在执行的任务或进行清理工作。这是主线程对 Worker 线程的一种单向控制。
-
self.close() 是在 Worker 线程内部调用的方法,用于优雅地关闭 Worker 线程。当你从 Worker 线程内部调用 close() 时,Worker 线程会完成当前正在执行的任务,然后关闭自己。这种方式允许 Worker 线程在关闭前进行必要的清理工作。
WebWorker 实战
页面上存在三个输入框,用户可以在任一输入框中输入数字后,输出对应的斐波那契值。
这里我们将分别给出非 WebWorker 实现、以及 WebWorker 实现两种方式加深对 WebWorker 的理解。
非 WebWorker Demo
首先,我们举一个不采用 Web worker 实现的示例,可以看到如果输出一个较大的值,整个网页会被阻塞住,我们没办法对网页进行任何操作。
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="box1">
<input type="number" value="0" />
<button onclick="clickHandler('box1')">计算</button>
<span></span>
</div>
<div id="box2">
<input type="number" value="0"/>
<button onclick="clickHandler('box2')">计算</button>
<span></span>
</div>
<div id="box3">
<input type="number" value="0"/>
<button onclick="clickHandler('box3')">计算</button>
<span></span>
</div>
<script>
function clickHandler(boxId) {
console.log('clickHandler', boxId);
const boxEl = document.getElementById(boxId);
const inputEl = boxEl.getElementsByTagName('input')[0];
const number = inputEl.value;
const result = fib(number);
const spanEl = boxEl.getElementsByTagName('span')[0];
spanEl.innerHTML = `${number} fib result is ${result}`;
}
function fib(n) {
if (n < 2) {
return n;
}
return fib(n - 1) + fib(n - 2);
}
</script>
</body>
</html>
WebWorker Demo
当我们采用 web worker 实现时,可以看到即使输入很大的值,也不会阻塞其他的输入。
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="box1">
<input type="number" value="0" />
<button onclick="clickHandler('box1')">计算</button>
<span></span>
</div>
<div id="box2">
<input type="number" value="0"/>
<button onclick="clickHandler('box2')">计算</button>
<span></span>
</div>
<div id="box3">
<input type="number" value="0"/>
<button onclick="clickHandler('box3')">计算</button>
<span></span>
</div>
<script>
const workerMap = {};
function clickHandler(boxId) {
let worker = workerMap[boxId];
if (!worker) {
worker = new Worker('fib_worker.js');
workerMap[boxId] = worker;
worker.onmessage = (event) => {
const boxEl = document.getElementById(boxId);
const inputEl = boxEl.getElementsByTagName('input')[0];
const number = inputEl.value;
const spanEl = boxEl.getElementsByTagName('span')[0];
spanEl.innerHTML = `${number} fib result is ${event.data}`;
}
}
const boxEl = document.getElementById(boxId);
const inputEl = boxEl.getElementsByTagName('input')[0];
const number = inputEl.value;
worker.postMessage(number);
}
</script>
</body>
</html>
javascript
function fib(n) {
if (n < 2) {
return n;
}
return fib(n - 1) + fib(n - 2);
}
self.onmessage = (event) => {
const number = event.data;
const result = fib(number);
self.postMessage(result);
}
注意事项
WebWorker 的使用需要遵循如下一些注意事项:
1、同源策略:分配给 Worker 线程运行的脚本文件,必须与主线程的脚本文件同源。
2、文件限制:Worker 脚本必须来源于网络文件,不能直接运行内敛 JavaScript 或从本地文件系统加载脚本。
3、作用域隔离:每个 Worker 都有自己的全局执行环境,不能在 Worker 中访问主线程中的JavaScript 对象。
4、结构化克隆:传递给Worker的数据通过结构化克隆算法进行复制。这意味着可以传递大多数JavaScript数据结构,但是不能传递DOM元素或函数。
5、DOM 限制:Worker 中无法读取 DOM 对比,也无法使用document、window、parent这些对象。但是,Worker 线程可以navigator对象和location对象。
应用场景
通过上面的体验,想必你知道了 WebWorker 基本的应用场景,即耗时长会阻塞主线程。以下是一些应用场景和代码示例:
复杂数学计算
在需要执行复杂计算的情况下,可以使用 Web Worker 来避免阻塞主线程。
javascript
// main.js
const worker = new Worker('calculator.js');
worker.onmessage = function(event) {
console.log('Result from worker:', event.data);
};
worker.postMessage({ type: 'calculate', data: [1, 2, 3, 4, 5] });
javascript
// calculator.js
self.onmessage = function(event) {
if (event.data.type === 'calculate') {
const result = event.data.data.reduce((sum, num) => sum + num, 0);
self.postMessage(result);
}
};
图像处理
在图像处理应用中,可以在 Worker 中执行图像分析或转换,以避免影响用户体验。
javascript
// main.js
const worker = new Worker('image-processor.js');
const image = document.getElementById('myImage');
worker.onmessage = function(event) {
const processedImage = event.data;
document.body.appendChild(processedImage);
};
worker.postMessage(image);
javascript
// image-processor.js
self.onmessage = function(event) {
const image = event.data;
// 假设有一个图像处理函数 processImage
const processedImage = processImage(image);
self.postMessage(processedImage);
};
数据解析
当需要解析大型 JSON 或 XML 数据时,可以使用 Worker 来处理,以避免阻塞主线程。
javascript
// main.js
const worker = new Worker('data-parser.js');
const data = { /* 大型数据对象 */ };
worker.onmessage = function(event) {
const parsedData = event.data;
console.log('Parsed data:', parsedData);
};
worker.postMessage(data);
javascript
// data-parser.js
self.onmessage = function(event) {
const data = event.data;
// 假设有一个解析函数 parseData
const parsedData = parseData(data);
self.postMessage(parsedData);
};
数据同步
在需要与服务器进行大量数据交换的应用中,可以使用 Worker 来处理数据同步,以避免影响用户体验。
javascript
// main.js
const worker = new Worker('worker.js');
worker.onmessage = function(event) {
console.log('Data received from worker:', event.data);
};
worker.onerror = function(error) {
console.error('Worker error:', error);
};
worker.postMessage({ type: 'fetchData', url: 'https://api.example.com/data' });
javascript
// worker.js
self.onmessage = function(event) {
if (event.data.type === 'fetchData') {
fetch(event.data.url)
.then(response => response.json())
.then(data => {
self.postMessage(data);
})
.catch(error => {
self.postMessage({ error: 'An error occurred while fetching the data.' });
});
}
};
Vue 中使用 Web Worker
在 Vue 中使用 Web Worker 略有不同,因为 Webpack、 Vite 等构建工具会对静态资源做处理,对于 Web Worker 而言,需要告知构建工具如何处理 .js 文件的 Worker。
这里以 Vite 为例,分别支持以下两种方式创建 Web Worker。
使用查询后缀导入
语法:
javascript
import MyWorker from './worker?worker'
const worker = new MyWorker()
javascript
<script setup>
import FibWorker from './workers/fib_worker?worker'
const workerMap = {};
function clickHandler(boxId) {
console.log('boxId', boxId);
let worker = workerMap[boxId];
if (!worker) {
worker = new FibWorker();
workerMap[boxId] = worker;
worker.onmessage = (event) => {
console.log('result', event.data);
const boxEl = document.getElementById(boxId);
const inputEl = boxEl.getElementsByTagName('input')[0];
const number = inputEl.value;
const spanEl = boxEl.getElementsByTagName('span')[0];
spanEl.innerHTML = `${number} fib result is ${event.data}`;
}
}
const boxEl = document.getElementById(boxId);
const inputEl = boxEl.getElementsByTagName('input')[0];
const number = inputEl.value;
worker.postMessage(number);
}
</script>
使用构造函数导入
语法:
javascript
const worker = new Worker(new URL('./worker.js', import.meta.url))
javascript
import MyWorker from './worker?worker'
const worker = new MyWorker()
javascript
<script setup>
const workerMap = {};
function clickHandler(boxId) {
console.log('boxId', boxId);
let worker = workerMap[boxId];
if (!worker) {
worker = new Worker(new URL('./workers/fib_worker.js', import.meta.url))
workerMap[boxId] = worker;
worker.onmessage = (event) => {
console.log('result', event.data);
const boxEl = document.getElementById(boxId);
const inputEl = boxEl.getElementsByTagName('input')[0];
const number = inputEl.value;
const spanEl = boxEl.getElementsByTagName('span')[0];
spanEl.innerHTML = `${number} fib result is ${event.data}`;
}
}
const boxEl = document.getElementById(boxId);
const inputEl = boxEl.getElementsByTagName('input')[0];
const number = inputEl.value;
worker.postMessage(number);
}
</script>
结尾
通过以上内容,想必你对 WebWorker 所能解决的问题有了深刻认识,根据其所能解决的问题就可以在项目中得以应用啦。如果你对于以上内容以及其他前端技能存在困惑,欢迎在评论区留言或添加我的个人微信交流。