likes
comments
collection
share

像进行网络请求一样使用 Web Worker

作者站长头像
站长
· 阅读数 16

前言

Web Worer 使得浏览器可以在 Worker 线程中运行脚本,而不会阻塞主线程。

Worker 线程中处理的数据大多数情况下最终是需要传到主线程中使用的。Worker 线程中的脚本和主线程的脚本之间通过 postMessage() 方法发送消息,通过监听 message 事件接受消息。如果一个 Worker 脚本执行一个单一的任务,那么主线程和 Worker 线程之间可以很方便地互相发送一次消息便能完成这个 Worker 脚本的任务。但是如果一个 Worker 脚本需要与主线程脚本频繁交互,甚至如果有一些第三方库需要在 Worker 线程中执行,而它又有很多 API 需要在主线程中决定如何去调用,这样的场景中怎样才能较好地管理 Worker 线程和主线程之间的消息传递呢?

worker-handler

worker-handler 旨在组织上述场景中两个线程之间的消息传递。

worker-handler 通过在 Worker 脚本中定义一系列 Action 函数来规定主线程可以要求 Worker 执行哪些操作。之后,在主线程中可以像进行网络请求一样向 Worker 发送请求和接收响应。有两种方式获得消息响应,可以通过 Promise 获取,就像 AJAX,一个请求对应一个响应,也可以通过 EventTarget 获取,就像 Server-sent events,一个请求可以收到多个响应,并且这两种响应方式可以在同一个请求中同时使用。

快速开始

以下是一个 worker-handler 最简的用法示例:

npm install worker-handler
// demo.worker.js
import { createOnmessage } from "worker-handler/worker";

// 传入 Actions 调用 createOnmessage 以创建 worker 的 onmessage 回调
onmessage = createOnmessage({
  // 如果只使用 Promise 响应方式,推荐使用 async 函数定义 Action
  async someAction() {
    // Action 中可以执行任意异步内容
    ......
    // 异步 Action 中返回的内容将作为响应内容以 promise 的形式传递给 Main
    return "some messages";
  }
});
// demo.main.js
import { WorkerHandler } from "worker-handler"; // 也可以从 "worker-handler/main" 中引入

// import workerUrl from "./demo.worker.ts?worker&url"; // in vite
// import workerInstance from "./demo.worker.ts?worker"; // in vite

const demoWorker = new WorkerHandler(
  // 如果是在 vite 环境中,可以传入上面的 workerUrl 或 workerInstance
  new Worker(new URL("./demo.worker.js", import.meta.url)) // webpack5 环境中以这种方式创建 Worker 实例
);

// 请求 Worker 执行 someAction
demoWorker.execute("someAction", []).promise.then((res) => {
  // 接收 Action 中以 Promise 形式响应的内容
  console.log(res.data);
}).catch((err) => {
  // Action 中发生的错误会使得 promise 被 reject
  console.log(err)
});

类型支持

typescript 中使用 worker-handler 时,定义好 Actions 的类型后,便可以在传递消息的发送端和接收端都能进行类型检测和提示,并且可以检测传递的消息是否可以被结构化克隆算法处理,是否需要处理可转移对象等。

以下是 typescript 中使用 worker-handler 的简单示例:

// demo.worker.ts
import { ActionResult, createOnmessage } from "worker-handler/worker";

/* 
 * 定义 Actions 的类型,之后有以下两处地方需要将其作为泛型参数传入:
 * - 在 Worker 中使用 createOnmessage() 时
 * - 在 Main 中使用 new WorkerHandler() 时
*/
export type DemoActions = {
  // 定义一个名为 pingLater 的 Action,其返回值类型 ActionResult<string> 表示该 Action 可以传递给 Main 的消息类型为 string
  pingLater: (delay: number) => ActionResult<string>;
};

onmessage = createOnmessage<DemoActions>({
  // pingLater 执行后会在 delay 毫秒后将消息传递给 Main
  async pingLater(delay) {
    await new Promise((resolve) => {
      setTimeout(() => {
        resolve(null);
      }, delay);
    });
    return "Worker recieved a message from Main " + delay + "ms ago.";
  }
});
// demo.main.ts
import { WorkerHandler } from "worker-handler/main";
import { DemoActions } from "./demo.worker";

const demoWorker = new WorkerHandler<DemoActions>(
  new Worker(new URL("./demo.worker.ts", import.meta.url))
);

demoWorker.execute("pingLater", [], 1000).promise.then((res) => {
  console.log(res.data);
});

响应消息

Promise 形式

有没有觉得上面的 pingLater 中想要实现指定特定延迟后再发送消息还需要自己 new 一个 Promise 很麻烦?这当然不合理,但是别担心,Promise 形式的消息传递除了使用 Action 的返回值,还支持通过调用 Action 中的 this.$end() 方法,可以在回调函数中使用,且同样支持类型检测和提示。所以上面的示例更适合这么写:

// demo.worker.ts
import { ActionResult, createOnmessage } from "worker-handler-test/worker";

export type DemoActions = {
  // 这里返回值类型被定义为 ActionResult<string | void>,表示传递的消息类型应为 string,并且该异步函数可能不会显式地返回一个值
  pingLater: (delay: number) => ActionResult<string | void>;
};

onmessage = createOnmessage<DemoActions>({
  async pingLater(delay) {
    setTimeout(() => {
      this.$end("Worker recieved a message from Main " + delay + "ms ago.");
    }, delay);
  }
});

this.$end() 方式适合 Action 在发出响应后仍需要继续执行的情况,或需要在 Action 中的回调函数中发出响应的情况,但是要注意,它无法在使用箭头函数定义的 Action 中使用。而函数返回值的方式适合当 Action 中所有逻辑执行完毕后再做出响应的情况,且可以在箭头函数中使用。

EventTarget 形式

如果需要一个请求对应多次响应,那么就需要使用 EventTarget 形式的响应。在 Action 中调用 this.$post() 可以将消息以 EventTarget 形式传递给主线程,同样, Action 不能被定义为箭头函数。

下面是一个同时使用 EventTargetPromise 形式响应消息,并在主线程中接收它们的示例:

// demo.worker.ts
import { ActionResult, createOnmessage } from "worker-handler-test/worker";

export type DemoActions = {
  // EventTarget 形式传递的消息类型也通过 Action 的返回值类型定义,ActionResult<string | void> 表示传递的消息类型是 string,并且该异步函数可能不会显式地返回一个值
  pingInterval: (
    interval: number,
    isImmediate: boolean,
    duration: number
  ) => ActionResult<string | void>;
};

// 调用 pingInterval() 后,每隔 interval 毫秒就会发送一次 EventTarget 形式的消息,在 duration 毫秒后会发送 Promise 形式的消息并关闭请求连接
onmessage = createOnmessage<DemoActions>({
  async pingInterval(interval, isImmediate, duration) {
    let counter = 0;
    const genMsg = () => "ping " + ++counter;
    if (isImmediate) this.$post(genMsg());
    const intervalId = setInterval(() => {
      this.$post(genMsg());
    }, interval);
    setTimeout(() => {
      clearInterval(intervalId);
      this.$end("no longer ping");
    }, duration);
  }
});
// demo.main.ts
import { WorkerHandler } from "worker-handler/main";
import { DemoActions } from "./demo.worker";

const demoWorker = new WorkerHandler<DemoActions>(
  new Worker(new URL("./demo.worker.ts", import.meta.url))
);

demoWorker
  .execute("pingInterval", [], 1000, false, 5000) // execute() 执行后会返回一个 MessageSource
  .addEventListener("message", (e) => {
    console.log(e.data);
  }) // 如果使用 addEventListener() 的方式监听 MessageSource,则会将 MessageSource 本身再次返回,使得可以链式调用
  .promise.then((res) => {
    console.log(res.data);
  });

执行 demo.main.ts 后,会在控制台输出如下内容:

像进行网络请求一样使用 Web Worker

调用 Action

Main 中执行 WorkerHandle 实例的 excute() 会与 Worker 产生一个连接,并执行一个 Action

excute() 接收的第三个以后的参数会按顺序传递给 Worker 中对应的 Action

第二个参数可以接收一个连接配置选项对象,包含 transfertimeout 两个属性:

  • transfer 是一个会被转移所有权到 Worker 中的的可转移对象数组。
  • timeout 是本次连接的超时时间。超时后该连接将会被关闭,不会再收到任何响应,且 Action 返回的 Promise 将转变为 rejected 状态。

也可以简化传参:

  • 如果只需要使用 transfer,可以直接传入一个数组。
  • 如果只需要使用 timeout,可以直接传入一个数字。
  • 如果都不需要开启,那么可以传入以下任意值:nullundefined[]、小于或等于 0 的任何数字。

Transfer

如果传递的消息中包含可转移对象,那么需要对其进行所有权进行转移处理。

主线程传递给 Worker 时,通过 workerHandle.execute() 的第二个参数进行指定。

Worker 传递给主线程时,Actionthis.$end()this.$post() 的第二个参数都是用来指定 transfer 数组的。

在使用 Action 的返回值进行响应时,返回值如果是一个数组,那么该数组只能有两个项,第一项是传递的消息,第二项就是指定的 transfer 数组。这也导致,如果 Action 返回值中如果希望传递数组类型的消息,必须通过 [messageData, [...transferable]] 的形式,即使不需要处理 transfer,例如:

// demo.worker.ts
import { ActionResult, createOnmessage } from "worker-handler/worker";

export type DemoActions = {
  getRandomNumsInArray: (amount: number) => ActionResult<number[]>;
};

onmessage = createOnmessage<DemoActions>({
  async getRandomNumsInArray(amount) {
    const numsArr = [];
    for (let i = 0; i < amount; i++) {
      numsArr.push(Math.round(Math.random() * 100));
    }
    // 如果这里是 "return numsArr",则 TS 类型检测不会通过
    return [numsArr, []];
  },
});

不过也不用担心不小心传错,当在 TS 中使用时,不符合要求的传参都会在写代码时就出现提示。

结语

以上基本涵盖了 worker-handler 的使用方式,具体的 API 可以在这里查看。

源码在这里,感兴趣的话给个 star 呗~

转载自:https://juejin.cn/post/7366516215378362418
评论
请登录