# WebWorker 的使用

# 有什么用

JavaScript 语言采用的是单线程模型,也就是说,所有任务只能在一个线程上完成,一次只能做一件事。前面的任务没做完,后面的任务只能等着。随着电脑计算能力的增强,尤其是多核 CPU 的出现,单线程带来很大的不便,无法充分发挥计算机的计算能力。

Web Worker 的作用,就是为 JavaScript 创造多线程环境,允许主线程创建 Worker 线程,将一些任务分配给后者运行。在主线程运行的同时,Worker 线程在后台运行,两者互不干扰。等到 Worker 线程完成计算任务,再把结果返回给主线程。这样的好处是,一些计算密集型或高延迟的任务,被 Worker 线程负担了,主线程(通常负责 UI 交互)就会很流畅,不会被阻塞或拖慢。

Worker 线程一旦新建成功,就会始终运行,不会被主线程上的活动(比如用户点击按钮、提交表单)打断。这样有利于随时响应主线程的通信。但是,这也造成了 Worker 比较耗费资源,不应该过度使用,而且一旦使用完毕,就应该关闭。

Web Worker 有几个注意点,

  1. 同源限制

    分配给Worker线程运行的脚本文件, 必须与主线程的脚本文件同源

  2. DOM限制

    Weoker线程所在的全局对象,无法读取主线程所在网页的DOM对象, 无法使用 docuemnt,window, parent 这些变量, 但是可以使用 navigatorlocation对象

  3. 通信联系

    Worker线程和主线程不在同一个上下文环境, 他们不能直接通信, 必须通过消息完成

  4. 脚本限制

    worker线程不能执行alert, confirm等方法, 但是可以使用 XMLHttpRequest对象发出Ajax请求

  5. worker线程无法读取本地文件, 就是不能打开本机的文件系统(file://), 加载的脚本必须来自网络

示例代码: WebWorker Demo (opens new window)

# 基本用法

可以先查看示例代码, 再查看。 这个示例代码要run的话, 简单点可以借助http-server (opens new window)即可。

主线程采用new命令, 调用Worker()构造函数, 新建一个Worker线程

if (window.Worker) {
  let worker = new Worker('worker.js');
}

参数是一个脚本文件, 该文件就是Worker线程所要执行的任务。这个脚本文件必须来自网络, 如果下载失败 ,worker也会初始化失败。

主线程调用 worker.postMessage()方法, 给Woker发送消息。

worker.postMessage('hello, world');
worker.postMessage({name: 'obj'});

postMessage的参数就是发给worker的数据, 它可以是各种类型的数据, 甚至是二进制文件。

主线程通过 message事件监听worker线程的数据回调

// 等同于 worker.onmessage 接收来自worker线程的消息
worker.addEventListener('message', event => {
  const { data } = event;
  console.log(`%cdata from worker: ${data}`, "color: red;");
  result_input.value = data
});

时间对象中的 data属性可以获取来自Worker线程的数据。

worker完成后, 主线程就可以关闭

function terminateWorker() {
  if (worker) {
    worker.terminate();
    worker = null;
  }
}

错误处理

主线程可以监听Worker是否发生错误, 如果发生错误, 可以使用主线程的 error 事件捕获。

// worker.onerror 或者下面的语法
worker.addEventListener('error', e => {
  result_input.value = ` ERROR: Line , ${e.lineno},  in , ${e.filename}, : , ${e.message}`;
});

worker 内部也可以监听 error 事件

# worker线程

worker线程内部也需要有一个监听函数, 监听message事件

self.addEventListener('message', onMessageHandler);

function onMessageHandler(event) {
  console.log(event);
  // 接收来自主线程的消息
  const { data } = event;
  console.log('%cdata from main thread: ' + data, 'color: green;');
  importScripts('./script.js')
  // 对数据操作, 这里一般是一些比较耗时的操作,比如各种费时的递归和排序等等
  const result = data.split('').reverse().join('');
  // 传递数据操作结果给主线程
  self.postMessage(result);
}
// 下面的方式是等价的
// this.addEventListener('message', onMessageHandler);
// onmessage = onMessageHandler

这里面的self和this 打印出来是DedicatedWorkerGlobalScope, 可以看做是这个worker对应的全局上下文。

我们也可以调用 self.close()在组件内部关闭自身。

# 加载外部脚本

worker内部如果需要加载其他的脚本,有一个专门的全局方法 importScripts可以加载其他的脚本

importScripts('./script.js')
// 支持同时加载多个脚本
importScripts('./script1.js', 'script2.js')
# 关闭worker

为了节省系统资源, 必须关闭Worker

// 主线程
worker.terminate();

// Worker 线程
self.close();

# 数据通信

主线程与Worker之间的通信内筒, 可以是文本, 也可以是对象。但是这种通信是值的拷贝, 不是引用的传递。worker对通信内容的修改, 不会影响到主线程。内部机制应该就是消息内容串行化之后发送给worker, worker再还原数据。

主线程与Worker中间也可以交换一些二进制数据, 比如Blob, File, ArrayBuffer等

const uIntArray = new Uint8Array(2);
uIntArray[0] = 100;
uIntArray[1] = 200;
worker.postMessage(uIntArray);

// Worker 线程
self.onmessage = function (e) {
  var uInt8Array = e.data;
  postMessage('Inside worker.js: uInt8Array.toString() = ' + uInt8Array.toString());
  postMessage('Inside worker.js: uInt8Array.byteLength = ' + uInt8Array.byteLength);
};

但是,拷贝方式发送二进制数据,会造成性能问题。比如,主线程向 Worker 发送一个 500MB 文件,默认情况下浏览器会生成一个原文件的拷贝。为了解决这个问题,JavaScript 允许主线程把二进制数据直接转移给子线程,但是一旦转移,主线程就无法再使用这些二进制数据了,这是为了防止出现多个线程同时修改数据的麻烦局面。这种转移数据的方法,叫做Transferable Objects (opens new window)。这使得主线程可以快速把数据交给 Worker,对于影像处理、声音处理、3D 运算等就非常方便了,不会产生性能负担。 下面的写法就是直接转义数据的控制权


// Transferable Objects 格式
worker.postMessage(arrayBuffer, [arrayBuffer]);

// 例子
var ab = new ArrayBuffer(1);
worker.postMessage(ab, [ab]);
# 同页面的WebWorker

一般的Web Worker载入的是一个单独的js脚本, 但是也可以载入与主线程在同一个网页的代码。

<script id="worker" type="text/worker">
  self.addEventListener('message', event => {
      self.postMessage(event.data + '----- from worker')
  })
</script>
<script>
  var blob = new Blob([document.querySelector('#worker').textContent]);
  var url = window.URL.createObjectURL(blob);
  var worker = new Worker(url);
  worker.onmessage = function (e) {
    // e.data === 'some message'
    console.log(e.data);
  };
  worker.postMessage('hello');
</script>

demo.html (opens new window)

# 示例

利用WebWorker来封装轮询, 只有当返回的数据变化时 触发 消息传递。 polling demo (opens new window)

function createWorker(f) {
  var blob = new Blob(['(' + f.toString() + ')()']);
  var url = window.URL.createObjectURL(blob);
  var worker = new Worker(url);
  window.URL.revokeObjectURL(url);
  return worker;
}

var pollingWorker = createWorker(function (e) {
  // 模拟获取数据
  function fetchData() {
    return Promise.resolve(Math.random() > 0.5 ? 'A' : 'B');
  }

  var cache;

  function compare(newV, oldV) {
    if (newV !== oldV) {
      return false;
    }
    return true;
  }

  self.addEventListener('message', ({ data }) => {
    if (data === 'init') {
      init();
    }
  });

  function init() {
    setInterval(function () {
      fetchData().then(function (res) {
        var data = res;
        console.log(data);
        if (!compare(data, cache)) {
          cache = data;
          self.postMessage(data);
        }
      });
    }, 1000);
  }
});

pollingWorker.onmessage = function ({ data }) {
  // render data
  const p = document.createElement('p');
  p.textContent = `changed  - ${data}`;
  document.body.appendChild(p);
};

pollingWorker.postMessage('init');	

# 总结一下

# 主线程

const myWorker = new Worker(jsUrl, options);	

Worker()构造函数,可以接受两个参数。第一个参数是脚本的网址(必须遵守同源政策),该参数是必需的,且只能加载 JS 脚本,否则会报错。第二个参数是配置对象,该对象可选。它的一个作用就是指定 Worker 的名称,用来区分多个 Worker 线程。

// 主线程
var myWorker = new Worker('worker.js', { name : 'myWorker' });

// Worker 线程
self.name // myWorker
  1. Worker.onerror:指定 error 事件的监听函数。

  2. Worker.onmessage:指定 message 事件的监听函数,发送过来的数据在Event.data属性中。

  3. Worker.onmessageerror:指定 messageerror 事件的监听函数。发送的数据无法序列化成字符串时,会触发这个事件。

  4. Worker.postMessage():向 Worker 线程发送消息。

  5. Worker.terminate():立即终止 Worker 线程。

# worker 线程

Web Worker 有自己的全局对象,不是主线程的window,而是一个专门为 Worker 定制的全局对象。因此定义在window上面的对象和方法不是全部都可以使用。

Worker 线程有一些自己的全局属性和方法。

  1. self.name: Worker 的名字。该属性只读,由构造函数指定。
  2. self.onmessage:指定message事件的监听函数。
  3. self.onmessageerror:指定 messageerror 事件的监听函数。发送的数据无法序列化成字符串时,会触发这个事件。
  4. self.close():关闭 Worker 线程。
  5. self.postMessage():向产生这个 Worker 线程发送消息。
  6. self.importScripts():加载 JS 脚本。
上次更新: 1/22/2025, 9:39:13 AM