关于 Web Worker

1,321 阅读5分钟

由于笔者是做大数据相关的业务,需要前端处理的数据量不定。最近就爆出了这么个 bug, 有一个 XX 客户,他的数据量呢,一下子就有200万条,结果我们的前端 Worker 就报错了。我们加上 Worker 的本意是为了避免过大的数据量导致自己页面的交互卡顿,没想到直接报错了。那么就由于这个错误,促使我去深入地了解 Worker 是什么,可以在我们的代码里面做什么。

为什么要用 Worker

众所周知,js 是一个单线程的语言,那么何为单线程语言呢?就是说,它执行任务的时候是串行执行的。执行完一个才可以去执行下一个。

而我们的 js 任务呢,不只是客户感受不到的任务,比如数据转化这种。还有更多的任务是会影响客户的交互体验的。比如你图表数据的转化还没有完成,客户已经开始跟页面进行交互了,那么这个时候 js 还没有空出时间来处理这个任务,那么带来的后果必然是页面交互卡顿。因此为了避免这种情况,我们希望在主线程之外还有其他的线程来帮我们处理这些任务,以便释放主线程去处理优先级更高的工作,这就是 Web Worker 诞生的原因。

什么是 Web Worker

拿 MDN 上面的定义来说呢,Web Worker 就是为 Web 内容在后台线程中运行脚本提供了一种简单的方法。线程可以执行任务但是不干扰用户界面,并且 Worker 线程可以和主线程进行交互。所以总结出来,Web Worker 就是一种可以不占用主线程资源的 JS 运行环境。Web Worker 和主线程的交互方法是 postMessage 和 onMessage,并且在数据传递的时候, worker 是使用拷贝的方式,这就是上面那个问题出现的原因。在 worker 进行拷贝的时候,由于数据量太大,导致浏览器内存无法承受,最终溢出。

Web Worker 的执行上下文

Web Worker 的执行上下文和主线程额执行上下文是两个完全不一样的空间。Web Worker 的执行上下文名称是 self,在其下是无法调用主线程的 window 对象的,并且不能操作dom。Worker 分为专用 Worker 和共享 Worker。标准 Worker 的上下文只在当前的脚本中被使用,而共享 Worker 可以跨脚本共享上下文。这里有 Worker 可以操作的 window 下的 API

如何使用 Worker

终于,在上面介绍了一堆关于 Web Worker 的相关知识后,我们终于可以介绍 Worker 的用法了。那这里我们写一个简单的例子哈。首先的一点呢,我们要说,想要用 Worker 的话,不要使用本地的 file 协议打开页面。因为使用 file 协议打开的页面下是没有 Worker 对象的哈, 必须使用 http 或者 https 协议。这是因为呢,Web Worker 遵循同源策略,而 file 协议没有定义 host 名字和端口号,所以在浏览器的同源策略中会被判定为不同源,所以浏览器会拒绝使用 file 协议来创建 Worker 实例。

那么我们就可以用构建工具,比如 webpack,首先来在本地起一个 http 服务。这个过程就不详述了,相信大家都能够做到,那么我们接下来会分两种类型来讨论 Worker。

标准 Worker

标准 Worker 是指区别于共享 Worker 的存在。下面我们就开始一个标准 Worker 的使用示例。

我们在页面上首先来写两个 button,点击某一个 button 的时候,会向 Worker 传递一个事件信息,标明当前点击的是哪个按钮。在 Worker 中会将当前的信息加以修饰再传回给主线程,然后 alert 出来。那么我们在主线程的代码如下:

index.js

if (window.Worker) {

    const first = document.getElementById('first');
    const second = document.getElementById('second');

    const myTask = new Worker(new URL('worker.js', import.meta.url));

    first.addEventListener('click', () => {
        myTask.postMessage('first');
    });

    second.addEventListener('click', () => {
        myTask.postMessage('second');
    });

    myTask.onmessage = (e) => {
        alert(e.data);
    };
}

worker.js

onmessage = function (event) {
    const result = `当前按下的是按钮${event.data}`;
    postMessage(result);
};

index.html

<!DOCTYPE html>
<html>
<head>
	<title>worker</title>
</head>
<body>
	<div>this is worker</div>
	<button id = 'first' style="width: 100px; height: 50px">first</button>
	<button id = 'second' style="width: 100px; height: 50px">second</button>
</body>
</html>

最终效果

温馨提示: 使用 webpack 的小伙伴注意 Worker 脚本的路径必须使用 new URL('worker.js', import.meta.url)这种方式引入哦。

image.png

共享 Worker

所谓的共享 Worker, 是指一个 Worker 可以同时被多个脚本共同使用。共享 Worker 的生成方式和标准 Worker 的很像,只是构造函数不同。

const myWorker = new SharedWorker(new URL('../index/worker.js', import.meta.url));

我们将这个 Worker 从脚本中导出

const myWorker = new SharedWorker(new URL('worker.js', import.meta.url));

export default myWorker;

之后在主页面和搜索页面都使用这个 worker

search.js

if (window.Worker) {
    myWorker.port.start();
    myWorker.port.postMessage('search');

    myWorker.port.onmessage = function (e) {
        alert(e.data);
    };
}

index.js

myTask.port.start();
myTask.port.postMessage('index');

myTask.port.onmessage = function (e) {
    alert(e.data);
};

效果

image.png worker.js

onconnect = function (event) {
    const port = event.ports[0];
    port.onmessage = (e) => {
        const result = `当前的页面是${e.data}`;
        port.postMessage(result);
    };
};

效果

image.png

在这里我们可以看到,除了构造函数不一样之外,更重要的是 Worker 开启的方式中,我们这里使用到了端口。使用 port 来发送和收取信息。

小技巧: 在调试 Worker 的时候,Chrome 提供了一个非常好用的方法, Worker 调试器

终止 Worker

我们可以在消息的发送方或者在 Worker 内部去终止 Worker, 终止 Worker 的方式有以下两种。

myWorker.terminate();

如果在 Worker 线程中进行中断,则使用

close();

错误处理

Worker 发生错误的时候会触发 onerror 事件,onerror 事件中会有三个用户关心的字段:

message: 可读性良好的错误字段

filename: 发生错误的脚本文件名

lineno: 发生错误时所在的脚本文件的行号

其他特征

在 Worker 中可以生成子 Worker, 但是这些子 Worker 必须托管在同源的父页面内。而且 subWorker 解析 URI 的时候会相对于父 Worker 的地址而不是自身页面的地址。

在 Worker 中可以使用importScripts()来引入脚本和库,这个方法可以同时支持多个脚本,脚本下载的顺序无法确定,但是脚本执行的顺序却是可以确定的,脚本加载的顺序会按照写入 importScripts 的顺序来执行。