突破 JS 单线程限制:Web Worker 实战指南

参考文档

线程和进程

进程(Process)

进程是计算机系统进行资源分配和调度的基本单位,是操作系统结构的基础。简单来说,进程就是一个正在执行的程序实例,包含程序代码和当前活动。

进程的特点:

  • 独立性:进程是系统中独立存在的实体,它可以拥有自己的资源

  • 隔离性:不同进程间的内存空间是相互隔离的,一个进程无法直接访问另一个进程的内存

  • 资源占用:每个进程都有自己的地址空间、内存、数据栈以及其他记录其运行状态的辅助数据

  • 系统开销大:进程创建、切换的开销较大

线程(Thread)

线程是进程中的一个执行流程,是CPU调度和分派的基本单位。一个进程可以有多个线程,线程共享进程的资源。

线程的特点:

  • 轻量级:线程比进程更轻量,创建和切换的开销更小

  • 资源共享:同一进程内的线程共享进程的内存空间和资源

  • 独立执行:线程有自己的执行栈和程序计数器,可以独立执行

  • 通信便捷:同一进程内的线程通信比进程间通信更简单高效

线程和进程的关系

  • 进程包含线程,一个进程至少有一个线程(主线程)

  • 线程是进程内的执行单元,多个线程共享进程的资源

  • 进程间通信较复杂(如IPC机制),线程间通信相对简单(可直接共享内存)

  • 进程是资源分配的基本单位,线程是CPU调度的基本单位

浏览器的进程

现代浏览器采用多进程架构,主要包括:

  • 浏览器进程:负责界面显示、用户交互、子进程管理等

  • 渲染进程:负责将HTML、CSS和JavaScript转换为用户可以交互的网页

  • 插件进程:负责插件的运行

  • GPU 进程:负责处理GPU相关的任务

  • 网络进程:负责网络资源加载

渲染进程的线程

渲染进程包含的线程:

  1. JS 引擎线程(主线程) :负责解析、执行JavaScript代码

  2. 渲染线程:负责渲染页面

  3. 定时器线程:负责setTimeout/setInterval的计时

  4. 事件触发线程:负责将满足触发条件的事件放入事件队列

  5. 异步HTTP请求线程:负责处理网络请求

JS是一门单线程的语言

JS是一门单线程语言,在某个时刻,主线程都只能执行一个同步任务。

这就会导致遇到IO操作,耗时计算时,都会阻塞主线程导致卡顿

JS是通过异步任务处理机制 ,也就是事件循环机制来解决这个问题的

  • 事件循环机制

    • JS 引擎遇到异步任务时,会将其交给浏览器提供的 Web API 对应的线程处理,在Nodejs中,是底层C++维护的线程池

    • JS 主线程继续往下执行

    • 当异步任务结束了,异步任务的 回调 会被放入"任务队列"中

    • JS 引擎会不断检查 调用栈 ,当调用栈为空,就会从任务队列中取一个任务放到 执行栈 中执行

    • 这个过程一直重复,也就是事件循环

  • 宏任务和微任务

    • 宏任务队列 (Macrotask Queue)【假定】 :存放的是一般的、独立的任务。

      1. script(整体代码块)
      2. setTimeout, setInterval
      3. I/O 操作(如文件读写、网络请求)
      4. UI 渲染,DOM 事件
      5. requestAnimationFrame (浏览器环境)
      6. setImmediate (Node.js 环境)
    • 微任务队列 (Microtask Queue) :存放的是需要尽快执行的、与当前任务紧密相关的任务。

      1. Promise.then(), .catch(), .finally()

      2. queueMicrotask

      3. MutationObserver

      4. process.nextTick (Node.js 环境,优先级高于其他所有微任务)

  • 事件循环的核心规则:

    • 执行完一个宏任务后,立即清空所有微任务,然后再进行 UI 渲染(如有必要),最后再开启下一个宏任务。

为什么有了JS异步机制还需要WebWorker

异步机制只解决了 JS 执行 IO 操作耗时的问题,涉及大量计算时,异步任务无法处理,必须由主线程处理

因此,W3C 为我们带来了HTML5中的一个特性------Web Worker 。它赋予了我们前端开发者在浏览器端开启真正 多线程的能力。

Web Worker 是浏览器提供的一个 API,它允许我们在主线程之外 创建一个或多个后台线程。这些后台线程可以执行脚本,但完全独立于主线程,两者通过消息传递(Message Passing)进行通信。

WebWorker线程安全

Worker 接口会生成真正的 操作系统 级别的线程,如果你不太小心,那么并发会对你的代码产生有趣的影响。

然而,对于 web worker 来说,与其他线程的通信点会被很小心的控制,这意味着你很难引起并发问题。你没有办法去访问非线程安全的组件或者是 DOM,此外你还需要通过序列化对象来与线程交互特定的数据。所以你要是不费点劲儿,还真搞不出错误来。

WebWorker的分类

WebWorker的分类包括:

暂时无法在飞书文档外展示此内容

我们通常说的 new Worker() 创建的是 专用工作线程 (Dedicated Worker) 。但实际上,Worker家族还有其他成员,以应对不同的场景。

  • 专用工作线程 (Dedicated Worker)

    • 定义 :由主线程脚本创建,并且仅能与创建它的那个主线程进行通信。
    • 生命周期:与创建它的页面(或主线程脚本)同生共死。当页面关闭时,它会立即被终结。
    • 用途:我们接下来讨论的大部分计算密集型场景,使用的都是它。
  • 共享工作线程 (Shared Worker)

    • 定义 :一种可以被多个浏览上下文 (Browse contexts) 共享的 Worker。这些上下文可以是同源 (same-origin) 的不同页面、iframe 或甚至是其他 Worker。

    • 通信方式 :与 Dedicated Worker 不同,通信必须通过一个 port 对象进行。每个希望连接的脚本都需要通过 worker.port 来建立连接和监听消息。

    • 应用场景

      • 多页面状态同步:用户打开了多个同站的标签页,可以通过一个 Shared Worker 来同步所有页面的状态,例如用户的登录信息、购物车内容。
      • 共享 WebSocket 连接:如果多个页面都需要与服务器建立 WebSocket 连接,可以由一个 Shared Worker 来统一管理这个连接,避免创建多个冗余的连接,节约客户端和服务器资源。
  • 服务工作线程 (Service Worker)

    • 定义 :这是一个更特殊、更强大的 Worker,它本质上是一个位于浏览器和网络之间的事件驱动的可编程网络代理

    • 特点

      • 独立生命周期:它的生命周期与页面完全无关,即使用户关闭了所有相关页面,它也可以在后台运行。
      • 无法访问 DOM:和所有 Worker 一样。
      • 拦截网络请求:可以拦截、处理和响应作用域内的所有网络请求。
      • 核心能力 :是实现渐进式 网络应用 ( PWA ) 的核心技术,能够带来丰富的原生应用级体验,如离线缓存、消息推送等。
    • 总结:Service Worker 的定位是"网络代理与事件中心",而 Dedicated/Shared Worker 的定位是"后台计算服务"。

WebWorker的特点

要理解 Worker,就要理解它的运行环境。每个 Worker 都拥有一个与主线程完全隔离的、独立的运行环境。具体表现为:

  • 独立的全局对象 :主线程的全局对象是 window,而在 Worker 线程中,全局对象是 selfthis。你无法在 Worker 中访问 window 对象。

  • 独立的 内存 空间:Worker 线程和主线程的内存是不共享的。这意味着你在一个线程中修改变量,不会影响到另一个线程。

  • 独立的事件循环 :每个 Worker 内部也有一套自己的事件循环机制。因此你可以在 Worker 内部使用 setTimeout, setInterval,甚至发起 fetch 请求。

正是因为这种彻底的隔离,才带来了那个最著名的限制:

  • 无法访问 DOM :由于多线程并发操作DOM会带来极其复杂的竞态问题(比如两个线程同时修改一个元素的内容),为了从根本上避免这种混乱,规范规定 Worker 线程中严禁 访问 document, alert 等与UI相关的API。Worker 的天职是"计算",而不是"渲染"。

主线程和Worker通信

主线程和Worker通信,依靠postMessage发消息和onmessage接受消息

  • 主线程 -> Worker
javascript 复制代码
 // main.js
const worker = new Worker('worker.js'); // 创建 Worker

// 发送消息
const dataToSend = { command: 'calculate', payload: [1, 2, 3] };
worker.postMessage(dataToSend);

// 监听来自 Worker 的消息
worker.onmessage = function(event) {
    console.log('Received from worker:', event.data); // { result: 6 }
};
  • Worker -> 主线程
ini 复制代码
 // worker.js
// 监听来自主线程的消息
self.onmessage = function(event) {
    const { command, payload } = event.data;
    if (command === 'calculate') {
        const result = payload.reduce((a, b) => a + b, 0);
        // 将结果发送回主线程
        self.postMessage({ result: result });
    }
};

可转移对象

postMessage 默认的通信方式是结构化克隆 (Structured Clone) 。这意味着当你发送一个对象时,浏览器会序列化这个对象,在另一端再反序列化来创建一个完整的副本

结构化克隆算法 - Web API | MDN

可转移对象 - Web API | MDN

所以其实postMessage是可以用来做深拷贝的

对于小数据,这没问题。但如果你要传输一个 500MB 的 ArrayBuffer(比如一个视频文件或大型三维模型数据),复制它本身就是一个极其耗时的操作,可能会导致几百毫秒甚至更长的卡顿,Web Worker 的性能优势将荡然无存。

对于某些特定类型的对象,如 ArrayBuffer, MessagePort, ImageBitmap,我们可以使用一种"零拷贝"的传输方式。

javascript 复制代码
 // main.js
const largeBuffer = new ArrayBuffer(1024 * 1024 * 8); // 8MB buffer

// 第二个参数是转移列表
worker.postMessage(largeBuffer, [largeBuffer]);

// 此时,主线程的 largeBuffer 将变得不可用
console.log(largeBuffer.byteLength); // 输出 0

postMessage 的第二个参数指定了哪些对象需要转移所有权 。这意味着 largeBuffer 的内存地址被直接交给了 Worker 线程,主线程将无法再访问它。这个过程几乎是瞬时的,无论数据多大。这是在处理海量数据时,必须掌握的核心优化技巧。

相关推荐
加个鸡腿儿7 小时前
异步函数中return与catch的错误处理
前端·javascript
黑狼传说7 小时前
深入探索V8引擎的编译机制:从JavaScript到机器码的完整之旅
前端·javascript·v8
kyle~7 小时前
计算机网络---CA证书体系(Certificate Authority)
前端·数据库·计算机网络
{⌐■_■}7 小时前
【JavaScript】读取商品页面中的结构化数据(JSON-LD),在不改动服务端情况下,实现一对一跳转
开发语言·javascript·json
前端fighter7 小时前
深入解析CSS定位:Sticky与Fixed的异同与实战应用
前端·css·面试
本就是菜鸟何必心太浮7 小时前
python中`__annotations__` 和 `inspect` 模块区别??
java·前端·python
Jerry7 小时前
Compose Material Design 系统
前端
Coodor7 小时前
碰一下可打开小程序,在web系统中如何嵌入将小程序写入NFC
前端·小程序·nfc