WebWorker
注意事项
- 一般工作用的不多
- 一般做cpu密集型的计算
- 不能操作 DOM
- Worker 线程所在的全局对象,与主线程不一样,无法读取主线程所在网页的 DOM 对象,也无法使用
document、window、parent这些对象。但是,Worker线程可以navigator对象和location对象。
- Worker 线程所在的全局对象,与主线程不一样,无法读取主线程所在网页的 DOM 对象,也无法使用
- 同源限制
- 通信联系
- Worker 线程和主线程不在同一个上下文环境,它们不能直接通信,必须通过消息完成。
- 脚本限制
- Worker 线程不能执行
alert()方法和confirm()方法,但可以使用XMLHttpRequest对象发出 AJAX 请求。
- Worker 线程不能执行
- 文件限制
- Worker 线程无法读取本地文件,即不能打开本机的文件系统(file://),它所加载的脚本,必须来自网络。
API限制
Details
Web Worker 是一个独立于主线程的运行环境,它的设计目的是为了执行一些耗时的任务而不阻塞主线程。由于 Web Worker 没有访问 DOM 的能力,因此许多与DOM 相关的 API 在 Worker 中不可用。以下是 Web Worker 中不能使用的主线程 API 和功能:
DOM 相关 API
- window
对象:Worker中没有window对象,因为它是浏览器的主全局对象,与 DOM 和 UI 相关。 - document 对象:Worker 无法直接访问 DOM,因此
document及其相关方法(如 document.getElementById、document.querySelector 等)不可用。 - UI 更新:Worker 无法直接操作 DOM 元素或更新页面内容。
- window
UI 相关 API
alert()、confirm()、prompt():这些浏览器弹窗方法与用户交互相关,只能在主线程中使用。console.log的局限性:虽然 console.log 在 Worker 中可用,但输出的内容只能在浏览器的 Worker 控制台中查看,而不是主线程的控制台。
主线程的全局对象和方法
localStorage和sessionStorage:这些存储 API 是同步的,且与主线程绑定,因此 Worker 中无法直接使用。可以使用IndexedDB或postMessage与主线程通信来间接访问。XMLHttpRequest:虽然XMLHttpRequest在 Worker 中可用,但更推荐使用fetch API。WebSocket:Worker 中可以使用 WebSocket,但需要手动管理连接和消息传递。
渲染相关 API
- Canvas 操作:Worker 无法直接操作
<canvas>元素。可以通过OffscreenCanvas(部分浏览器支持)将Canvas的控制权转移到 Worker 中。 WebGL:Worker 中无法直接使用 WebGL,但可以通过OffscreenCanvas实现。
- Canvas 操作:Worker 无法直接操作
浏览器特定功能
- 地理位置 API(
navigator.geolocation):Worker 中无法直接使用,需要通过主线程获取位置信息后传递到 Worker。 - 媒体相关 API(如
navigator.mediaDevices):Worker 中无法直接访问摄像头、麦克风等设备。 - 剪贴板 API(
navigator.clipboard):Worker 中无法直接操作剪贴板。 - 剪贴板 API(
navigator.clipboard):Worker 中无法直接操作剪贴板。
- 地理位置 API(
同步 API
- 同步的 XMLHttpRequest:Worker 中只能使用异步的 XMLHttpRequest,同步版本不可用。
- 同步的 FileReaderSync:Worker 中可以使用 FileReaderSync,但主线程中不可用。
其他限制
- importScripts 的限制:Worker 中可以使用
importScripts加载外部脚本,但加载的脚本必须是同源的,且会阻塞 Worker 线程。 - 无法创建新的 Worker:Worker 中无法再创建嵌套的 Worker(即 new Worker() 不可用)。
- importScripts 的限制:Worker 中可以使用
Worker 中可用的 API
- 虽然 Worker 中有很多限制,但它仍然支持许多重要的 API,例如:
- setTimeout 和 setInterval:用于定时任务。
- fetch:用于网络请求。
- IndexedDB:用于存储大量结构化数据。
- WebSocket:用于实时通信。
- FileReader:用于读取文件内容。
- Atomics 和 SharedArrayBuffer:用于多线程之间的共享内存操作(需要谨慎使用)。
Web Worker中定时器的限制与性能分析
Details
将 JavaScript 中的倒计时和定时器放入 Web Worker 并通过 postMessage 进行通信,虽然可以避免阻塞主线程,但也存在一些限制、性能缺陷和潜在隐患:
- 通信开销
- 频繁通信的性能损耗:postMessage 涉及主线程与 Worker 之间的数据序列化和反序列化,频繁通信会增加性能开销,尤其在数据量较大时。
- 延迟问题:消息传递存在延迟,可能导致倒计时或定时器的执行不够精确。
- 定时器精度
- Worker 中的定时器精度受限:Web Worker 中的 setTimeout 和 setInterval 精度不如主线程,尤其在浏览器处于后台或设备资源紧张时,可能导致定时器不准确。
- 资源消耗
- 额外的内存和 CPU 开销:每个 Worker 都会占用独立的内存和 CPU 资源,过多的 Worker 会增加整体资源消耗。
- Worker 生命周期管理:需要手动管理 Worker 的创建和销毁,否则可能导致内存泄漏。
- 兼容性和调试
- 兼容性问题:虽然现代浏览器普遍支持 Web Worker,但在某些旧版浏览器或特殊环境中可能不支持。
- 调试困难:Worker 中的代码调试比主线程复杂,错误排查和日志记录不够直观。
- 潜在隐患
- 消息丢失或乱序:在高负载或网络不稳定的情况下,消息可能丢失或乱序,影响倒计时或定时器的准确性。
- 竞态条件:如果多个 Worker 或主线程同时操作共享数据,可能引发竞态条件,需额外处理同步问题。
- 适用场景
- 适合的场景:需要长时间运行或高频率触发的任务,且不涉及 DOM 操作。
- 不适合的场景:需要高精度定时或频繁更新 UI 的任务。
主线程定时器精度问题
Details
在浏览器中,主线程的定时器精度通常比 Worker 中的定时器精度更高。以下是具体原因和对比:
主线程定时器的精度
- 更高的优先级:主线程是浏览器渲染和用户交互的核心,定时器(如 setTimeout 和 setInterval)在主线程中通常会被优先调度。
- 更小的延迟:主线程的定时器能够更快响应,尤其是在页面处于前台(active)状态时。
- 受浏览器优化影响:现代浏览器会对主线程的定时器进行优化,例如将多个短间隔的定时器合并,以减少性能开销。
Worker 中的定时器精度
- 较低的优先级:Worker 是后台线程,其定时器的调度优先级通常低于主线程。
- 更高的延迟:Worker 中的定时器可能受到浏览器后台调度策略的影响,尤其是在页面处于后台(inactive)状态时,延迟会更明显。
- 受设备性能影响:在低性能设备或高负载情况下,Worker 中的定时器延迟可能会进一步增加。
影响定时器精度的因素
- 页面状态:
- 如果页面处于前台(用户正在交互),主线程和 Worker 的定时器精度都会较高。
- 如果页面处于后台(例如最小化或切换到其他标签),浏览器可能会降低定时器的精度,尤其是 Worker 中的定时器。
- 浏览器优化:
- 现代浏览器(如 Chrome、Firefox)会对后台页面的定时器进行节流(throttling),以减少资源消耗。Worker 中的定时器更容易受到这种节流的影响。
- 设备性能:
- 在低性能设备上,Worker 中的定时器延迟可能会更明显。
- 页面状态:
实际测试结果
- 根据实际测试,主线程的定时器通常能够更接近预期的触发时间(例如 setTimeout(fn, 10) 可能在 10-12ms 后触发)。
- Worker 中的定时器可能会有更大的偏差(例如 setTimeout(fn, 10) 可能在 15-20ms 后触发)。
适用场景
- 主线程定时器:
- 适合需要高精度定时器的场景,例如动画、游戏或实时 UI 更新。
- 缺点是可能阻塞主线程,导致页面卡顿。
- Worker 定时器:
- 适合不需要高精度但需要长时间运行的任务,例如后台数据处理、轮询等。
- 优点是不会阻塞主线程,但精度较低。
- 主线程定时器:
使用案例
Details
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<input type="text" value="" id="text1">
<input type="text" value="" id="text2">
<input type="button" id="btn">
<script>
window.onload = function () {
let text1 = document.getElementById( 'text1' );
let text2 = document.getElementById( 'text2' );
let btn = document.getElementById( 'btn' );
let workers = new Worker( 'worker.js' );
btn.onclick = function () {
let n1 = parseInt( text1.value );
let n2 = parseInt( text2.value );
// 1、创建 worker 对象
// 2、发送数据
workers.postMessage( { n1, n2 } );
// 5、接收处理结果
workers.onmessage = function ( ev ) {
console.log( ev.data );
// 6、关闭线程
workers.terminate();
};
};
};
</script>
</body>
</html>js
// 3、接收数据
this.onmessage = function ( ev ) {
let { n1, n2 } = ev.data;
// 4、发送处理结果
this.postMessage( n1 + n2 );
};Worker 内使用其他的普通js 文件
Details
importScripts方法确实不支持 ES 模块(ESM)。它只能加载传统的 JavaScript 文件,这些文件使用 var、function 等语法定义全局变量和函数。对于 ES 模块,你需要使用其他方式来加载模块。
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>
<input type="button" id="btn" value="sendMessage" />
<script>
const worker = new Worker('/worker.js')
const btn = document.getElementById("btn")
btn.addEventListener('click', () => {
worker.onmessage = function (e) {
console.log(`worker.js${e}`);
}
worker.postMessage({ a: 1 })
})
</script>
</body>
</html>js
importScripts("/other.js");
this.onmessage = function (e) {
log(e);
};js
function log(e) {
console.log(e);
}Worker加载esModule文件
Details
使用动态引入
jslet func = null; import("/other.js").then((res) => { func = res; }); this.onmessage = function (e) { func.log(e); };使用
<script type="module">标签(仅限主线程)- 如果你需要在主线程中加载 ES 模块并传递给 Worker,可以先在主线程中加载模块,然后将必要的数据或方法通过 postMessage 发送给 Worker。
html<script type="module"> import { someFunction } from './module.js'; const worker = new Worker('worker.js'); worker.postMessage({ someFunction }); </script>jsonmessage = function(e) { const { someFunction } = e.data; // 使用传递过来的函数 someFunction(); };将 ES 模块转换为传统脚本
js// webpack npx webpack --entry ./src/module.js --output ./dist/bundle.js // rollup npx rollup main.js --file bundle.js --format iife npx rollup main.js --file bundle.js --format cjs
worker 路径
Details
js
const worker = new Worker(new URL('./worker.js', import.meta.url));
worker.postMessage('Hello from main thread!');
worker.onmessage = (event) => {
console.log('Message from worker:', event.data);
};