使用 Web Worker 实现简单的非阻塞异步

以前的文章提到了 JavaScript 中的异步编程,然而不管早就存在的 setTimeout 仍是 ES6 中的 Promise,它们都是 阻塞 异步,执行函数的时候,会阻塞线程。setTimeout 只会把一个函数延后执行,但仍是在主线程中执行,执行函数的时候会阻塞线程。换句话说,setTimeout 只实现了过程间并发(concurrent)而未实现并行(parallel)。javascript

ES 规范并无定义多线程,Node.js 至今也没有原生的多线程实现。然而在 HTML5 中却定义了 Web Worker 用于实现浏览器中的多线程。java

Web Worker

引用 MDN 原文:git

Web Workers 使得一个Web应用程序能够在与主执行线程分离的后台线程中运行一个脚本操做。这样作的好处是能够在一个单独的线程中执行费时的处理任务,从而容许主(一般是UI)线程运行而不被阻塞/放慢。

与朴素(原始)的多线程编程方式不一样,Web Worker 一般不容许线程间共享数据,因此没有线程同步、数据竞争等问题,更没有没有锁(Mutex)和条件变量(Condition variable)等概念(注 1)。它们使用 postMessage 相互通讯,能够认为是 JS 中的参与者模式实现。各个 Worker 间数据独立,不共享内存:postMessage 始终经过结构化克隆的方式深拷贝传值。github

使用 Web Worker 也很是简单,只须要预先在 Worker 中注册 message 事件,在主线程中 postMessage 给 Worker 处理就行了。处理完后能够再经过 postMessage 传结果给主线程。编程

须要注意的是,Web Worker 中不能够操做 DOM,一切与 DOM 操做相关的函数、类都不能使用(建立一个 DOM 元素发回给主线程 appendChild 也不行),因此可使用的方法很是有限,只适用于处理数据(注 2)。segmentfault

使用 Web Worker 实现非阻塞的 Promise

前面提到 Promise 是阻塞异步,那是否能够把要处理的数据转发给某个 Worker 处理并返回一个 Promise,在处理完后将其 resolve 掉呢?数组

答案固然是能够的,并且实现并不复杂。浏览器

建立 Web Worker

首先固然是 new 一个 Worker 出来。须要注意的是 Worker 的构造函数 接受的是一个 JavaScript 脚本的 URL,能否接受 data-uri 看浏览器,实测 Chrome、Firefox 能够,Safari、Edge 不行(会抛 SECURITY_ERR 异常)。多线程

简单起见,这里仍是采起 data-uri 的形式。考虑可移植性的话能够先指定一个静态文件,而后使用 postMessage 把函数体传过去。并发

this._worker = new Worker('data:text/javascript,' + encodeURIComponent(`'use strict';
const __fn = ${fn};
onmessage = e => postMessage(__fn(...e.data));`));

Worker 中作了两件事:

  1. 定义一个函数变量 __fn,其值 fn 是须要执行的函数。若是 fn 自己是一个函数对象,这里将其转换为字符串,至关于把函数的源代码拼到了字符串里。
  2. 绑定 message 事件。将传入的值做为参数列表调用 __fn,而后将 __fn 的返回值经过 postMessage 传给主函数。

当接受请求时,派发事件给建立的 Worker

function dispatch(...args) {
  return new Promise((resolve, reject) => {
    this._queue.push({ resolve, reject });
    this._worker.postMessage(args);
  });
}

返回一个 Promise。注意这里不能只是简单的 postMessage。由于若是使用者屡次调用 dispatch 函数一次建立了多个 Promise,以后很难肯定是哪一个 Promise 完成了。这里经过一个队列记忆建立的 Promise 顺序,而后依次 resolve(单个 Worker 处理 message 事件仍是顺序执行的)。固然你也能够多传一个标记值给 Worker 用于标记被 resolve 的 Promise。

JavaScript 里的队列就是数组:

this._queue = [];

接收 Worker 处理完返回的值

this._worker.onmessage = e => this._queue.shift().resolve(e.data);
this._worker.onerror = e => this._queue.shift().reject(e.error);

onmessage 表示正常返回;onerror 表示出现了异常。对应的 Promise 的 resolve 和 reject 直接从队列里取出来。

完整代码

class Dispatcher {
  constructor(fn) {
    this._queue = [];
    this._worker = new Worker('data:text/javascript,' + encodeURIComponent(`'use strict';
const __fn = ${fn};
onmessage = e => postMessage(__fn(...e.data));`));
    this._worker.onmessage = e => this._queue.shift().resolve(e.data);
    this._worker.onerror = e => this._queue.shift().reject(e.error);
  }

  dispatch(...args) {
    return new Promise((resolve, reject) => {
      this._queue.push({ resolve, reject });
      this._worker.postMessage(args);
    });
  }
}

这就是完整代码了,总共不到 20 行。使用的话也很简单:

const dispatcher = new Dispatcher(arr => { // 建立对象,把入口函数传入
  for (let i=0; i<1000; ++i) arr.sort(); // 耗费些时间
  return arr;  // 返回处理后的结果
});

const arr = Array.from({ length: 8192 }, () => Math.random() * 10000); // 须要处理的数据
dispatcher.dispatch(arr)  // 派发给 Worker
  .then(res => console.log(res));  // 处理完毕后输出

在浏览器中测试,会生成这样一段代码:

clipboard.png

排序大数组 1000 次的同时 UI 响应仍然不受影响。

这里还有一个线程池的版本,能够建立多个 Worker 同时并行执行多个任务:https://github.com/CarterLi/T...

由于要区分到底是哪一个 Worker 完成运行,处理 Worker 返回值的逻辑复杂了一些,有什么建议欢迎提出。

  • 注 1:ES2017 中加入 SharedArrayBuffer 后已经能够在主线程和各 Web Worker 间共享数据,使用 Atomics.wait()Atomics.wake() 还能够实现传统意义上的锁和条件变量。但因为其出现较晚且并不是使用 Web Worker 的主流方式,这里不展开讨论。
  • 注 2:还有一个多是在 Worker 中画图,见 OffscreenCanvas。一旦实现,对游戏编程是个不小的帮助。
相关文章
相关标签/搜索