Web Worker——JS 的多线程

3460

这是我参与11月更文挑战的第4天,活动详情查看:2021最后一次更文挑战

我们经常说 JavaScript 是单线程,是指相对于 JAVA 等语言,JavaScript 不具备并行任务处理的特性。JS 具有一个主线程,所有的任务需要排队进行处理。主线程在执行前面的任务时,会阻塞后面的任务。

随着计算机处理能力的提高,这种单线程模型无法充分发挥多核 CPU 计算机的计算能力。为了让 JS 也能够有多线程能力,诞生了 Web Worker 。

现在我们可以在主线程中创建 worker 线程,将一些计算密集型或高延迟的任务分配给 worker 线程运行。worker 线程与主线程并行运行,互不干扰,这就避免了单线程任务阻塞的情况。

基本用法

首先需要注意一些限制:

  1. 同源限制:创建 worker 线程的时候需要分配一个 JS 文件,该文件必须是同源的,且不能是本地文件;
  2. 环境隔离:worker 线程所在的上下文环境与主线程不一样,无法读取网页的 DOM 对象,全局对象不再是 window,可以通过 this 或 self 访问。
  3. 通信受限:主线程和 worker 线程不能直接通信,通过 postMessage 方法进行消息传递。

在主线程中,调用 worker 构造函数,创建 worker 线程:

let worker = new Worker('work.js')  // 必须传递一个 url,受同源限制

主线程通过调用 postMessage 方法,向 worker 线程传递数据,数据类型不限:

worker.postMessage('Hello World');
worker.postMessage({a:1, b:2});

主线程通过监听 onmessage 事件,接收子线程传递的信息:

worker.onmessage = function(event) {
    console.log(event.data); // data 就是 worker 传递的数据
}

woker 线程内同样通过监听 onmessage 事件,接收主线程传递的数据,通过 postMessage 传递数据:

self.addEventListener('message', function(event) {
    console.log(event.data); // data 为传递的数据
    // do something...
    self.postMessage('get✔');
})

错误处理:

// 主线程
worker.onerror(function (event) {
  console.log([
    'ERROR: Line ', e.lineno, ' in ', e.filename, ': ', e.message
  ].join(''));
});

// 子线程
self.onerror(function (event) {
  console.log([
    'ERROR: Line ', e.lineno, ' in ', e.filename, ': ', e.message
  ].join(''));
});

在 woker 线程执行完毕之后,需要关闭线程,释放资源:

// 主线程
worker.terminate();

// 子线程
self.close();

在 HTML 中使用 worker

worker 除了载入同源的 JS 脚本文件外,也可以是同一页面的代码。在页面中创建一段浏览器不解析的标签,转成 blob url 后就可以作为参数传递给 worker 构造函数了,例如:

<!-- 写一个浏览器不认识的 type,这段脚本就不会被解析执行 -->
<script id="worker" type="app/worker">
    console.log(self);

    self.onmessage = function(event) {
        console.log('子线程收到消息:', event.data);

        self.postMessage('get✔');
    }
    self.onerror = function (err) {
        console.log('子线程异常:', err);
    }

    throw new Error('test error');
</script>

然后再将脚本内容转成 blob-url,创建 worker 线程运行:

<script>
    let blob = new Blob([document.querySelector('#worker').textContent]);
    let url = window.URL.createObjectURL(blob);
    let worker = new Worker(url);

    worker.onmessage = function(event) {
        console.log('主线程收到消息:', event.data);
    }
    worker.onerror = function (err) {
        console.log('主线程收到子线程异常:', err);
    }

    worker.postMessage('Hello World');
</script>

完整代码如下,试试看会输出什么:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Web Worker</title>
</head>
<body>
    <script id="worker" type="app/worker">
        console.log(self);

        self.onmessage = function(event) {
            console.log('子线程收到消息:', event.data);

            self.postMessage('get✔');
        }
        self.onerror = function (err) {
            console.log('子线程异常:', err);
        }

        throw new Error('test error');
    </script>
    <script>
        let blob = new Blob([document.querySelector('#worker').textContent]);
        let url = window.URL.createObjectURL(blob);
        let worker = new Worker(url);

        worker.onmessage = function(event) {
            console.log('主线程收到消息:', event.data);
        }
        worker.onerror = function (err) {
            console.log('主线程收到子线程异常:', err);
        }

        worker.postMessage('Hello World');
    </script>
</body>
</html>

在 vue 项目中使用 worker

webpack | vite

如果项目使用的 webpack 4,需要安装 worker-loader,参考文档说明。webpack 5 则不需要安装 worker-loader,具体使用可以参考文档说明

vite 不需要安装插件,通过 ?worker?sharedworker 进行导入,默认导出一个自定义的 worker 构造器。具体使用可以参考文档说明

演示

先使用 vite 创建了一个 vue 项目:

$ npm init vite@latest vue-worker --template vue
$ npm i
$ npm run dev

页面展示如下:

image.png

接下来我们利用 worker 实现功能:点击 count 按钮的时,计算出 count 对应的 斐波那契数,进行展示。

首先编写一个 worker.js 文件,进行斐波那契数的计算:

// /src/worker.js
/**
 * 计算斐波那契数
 * @param {number} n 
 * @returns {number} 返回第 n 个斐波那契数
 */
function fib(n) { 
    let arr = [0,1];
    for (let i = 2; i <= n; i++) {
        arr[i] = arr[i-1] + arr[i-2];
    }
    return arr[n];
}

self.onmessage = function (e) {
    let n = e.data;
    self.postMessage(fib(n));
    self.close();
}

修改 HalloWorld.vue 文件,引入 worker,增加侦听器和 fibCount 属性,在页面展示:

import { ref, watch } from 'vue'
import myWorker from '../worker.js?worker'  // 引入 worker 文件,通过 ?worker 标识

defineProps({
  msg: String
})

const count = ref(0)
const fibCount = ref(0);

watch(count, (val) => { // 监听到 count 改变的时候,创建 worker 线程计算斐波那契数列。
  let worker = new myWorker;
  worker.postMessage(count.value);
  worker.onmessage = (e) => {
    console.log(e.data);
    fibCount.value = e.data;
  }
});

增加展示标签:

<button type="button" @click="count++">count is: {{ count }}</button>
++ <p>第 {{ count }} 个斐波那契数为 {{ fibCount }}</p>

是不是很简单,运行看看效果如何:

image.png

最后,我们再看看打包后的文件是怎么样的:

$ npm run build

生成 dist 文件夹的文件如下:

image.png

可以看到 worker.js 被打包成单独的一个文件,进入 index.e375bf05.js 文件搜索关键词,看到 worker 构造函数通过 url 进行载入:

image.png

总结

  1. 使用 worker 构造函数创建 worker 线程,需要传递一个同源脚本文件 URL;
  2. 使用 postMessageonmessage 方法事件进行消息传递;
  3. 通过 onerror 事件处理异常,使用 close 方法关闭线程;
  4. worker 线程任务可以是 JS 脚本文件,也可以是内联脚本;
  5. 在项目中使用 worker,webpack 4 需要安装 worker-loader,webpack 5 和 vite 原生支持。

参考

Web Worker

Web Workers API