🎉🎉🎉 Web Workers 使用秘籍,祝您早日通关前端多线程!

2,278 阅读9分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第31天,点击查看活动详情

Web Workers 是新一代的异步编程解决方案,它可以让我们在后台运行一个脚本,而不会阻塞用户界面。

对于前端开发者来说,Web Workers 是一个非常有用的工具,它可以让我们在后台运行一些耗时的任务,比如计算、数据处理等,而不会阻塞用户界面。

接下来就带你正式上手 Web Workers。

开始之前的准备工作

根据评论区的小伙伴的需求,特地补上这一说明。

Web Workers是需要运行在服务环境中(http/https协议),也就是如果我们通过本地直接预览html是不行的(file协议),这个时候解决方案有很多,最简单的解决方案是通过 ide,下面就介绍各种解决方案。

WebStorm 用户

我就是WebStorm的用户,可以直接在html文件中右键点击,然后选择运行 or 调试都可以。

image.png

vscode 用户

vscode可以在vscode中安装Live Server插件;

安装成功后,用vscode打开html文件所在的文件夹

vscode中直接右击 Open with Live Server打开即可!

不想装插件?

不想装插件就麻烦一些了:

  1. 可以直接下载tomcat或者nginx在自己的电脑上面跑一个服务。
  2. 可以通过http-server来开启一个服务,npm install http-server -g
  3. 使用node来搭建一个服务环境,node有很多插件包可以达到这个效果。

方法有很多,开阔思路最重要。

1. 什么是 Web Workers

Web Workers 是一个新的JavaScript API,它可以让我们在后台运行一个脚本,而不会阻塞用户界面。

它是独立于主线程的一个线程,当然它为了不阻塞主线程,也有一些限制,比如不能访问DOM,也不能访问其他脚本创建的变量。

因为有上面的限制,所以Web Workers不想多线程编程语言一样,有锁的概念,也不会有线程安全的问题。

它的使用方式非常简单,只需要创建一个Worker对象,然后调用它的postMessage方法,就可以在后台运行一个脚本了。

现在我们来看一个简单的例子:

  • main.js
// main.js
// 创建一个 Worker 对象
const worker = new Worker('worker.js');

// 调用 postMessage 方法,传递一个消息
worker.postMessage('Hello World!');
  • worker.js
// worker.js
// 监听消息
self.addEventListener('message', (event) => {
    console.log(event.data);
});

在上面的例子中,我们在main.js中创建了一个Worker对象,然后调用它的postMessage方法,传递了一个消息。

worker.js中,我们监听了message事件,当main.js中的Worker对象调用postMessage方法时,就会触发message 事件,我们就可以在事件回调中获取到传递过来的消息。

注意:worker.js中的self指向的是WorkerGlobalScope对象,它是Worker对象的全局作用域,它的addEventListener方法用来监听事件。

2. 传递数据

上面的示例中,我们只是传递了一个字符串,但是实际上,我们可以传递任何数据类型,比如ArrayBufferBlobMessagePort等。

我们来看一个例子:

  • main.js
// main.js
const worker = new Worker('worker.js');

// 创建一个 ArrayBuffer 对象
const buffer = new ArrayBuffer(16);

// 创建一个 Int32Array 对象
const int32View = new Int32Array(buffer);

// 设置 Int32Array 对象的值
for (let i = 0; i < int32View.length; i++) {
    int32View[i] = i * 2;
}

// 传递一个 ArrayBuffer 对象
worker.postMessage(buffer);
// 传递一个 Int32Array 对象
worker.postMessage(int32View);
  • worker.js
// worker.js
self.addEventListener('message', (event) => {
    // 获取 ArrayBuffer 对象
    const buffer = event.data;

    // 创建一个 Int32Array 对象
    const int32View = new Int32Array(buffer);

    // 打印 Int32Array 对象的值
    for (let i = 0; i < int32View.length; i++) {
        console.log(int32View[i]);
    }
});

在上面的例子中,我们在main.js中创建了一个ArrayBuffer对象,然后创建了一个Int32Array 对象,最后把这两个对象都传递给了Worker对象。

worker.js中,我们监听了message事件,然后获取到了传递过来的对象,然后创建了一个Int32Array对象,最后打印了这个对象的值。

这里有一个问题就是我们如何知道传递过来的是ArrayBuffer对象还是Int32Array对象呢?

这里有很多种方法可以判断,比如我们可以在传递的时候,把对象的类型也传递过去,或者我们可以在传递的时候,把对象的类型作为key ,对象作为value,然后在worker.js中,通过key来获取到对象。

这里我只是引出一个问题,就是web worker中,我们只有一个message事件,同时我们可以传递任何JavaScript 对象,所以我们可以根据自己的需求,来定义传递的数据格式。

例如可以定义一个对象,然后把对象的类型作为key,对象作为value,然后在worker.js中,通过key来获取到对象。

// main.js
const worker = new Worker('worker.js');

// 创建一个 ArrayBuffer 对象
const buffer = new ArrayBuffer(16);

// 创建一个 Int32Array 对象
const int32View = new Int32Array(buffer);

// 传递一个 ArrayBuffer 对象
worker.postMessage({
    type: 'ArrayBuffer',
    data: buffer
});

// 传递一个 Int32Array 对象
worker.postMessage({
    type: 'Int32Array',
    data: int32View
});

这里就说这么多了,接下来我们来看一下web worker是怎么把数据传递给主线程的。

3. 传递数据给主线程

web worker中,我们可以通过postMessage方法来向主线程传递数据,这个方法的参数可以是任何JavaScript对象,比如StringNumberBooleanArrayObject等。

是的worker中同样也有postMessage方法,用于向主线程传递数据。

// worker.js
self.addEventListener('message', (event) => {
    // 向主线程传递数据
    self.postMessage('收到了!!!');
});

在上面的例子中,我们在worker.js中监听了message事件,然后在事件处理函数中,向主线程传递了一个String对象。

在主线程中,我们可以通过Worker对象的onmessage属性来监听worker传递过来的数据。

// main.js
const worker = new Worker('worker.js');

worker.onmessage = (event) => {
    console.log(event.data);
};

在上面的例子中,我们在主线程中创建了一个Worker对象,然后监听了worker传递过来的数据。

是不是很简单,主线程通过postMessage方法向worker传递数据,worker也是通过postMessage方法向主线程传递数据。

不同的是主线程通过onmessage属性来监听worker传递过来的数据,而worker通过addEventListener方法来监听主线程传递过来的数据。

4. 异常处理

web worker中,如果遇到了异常,它是不会抛出异常的,而是会触发error事件。

也不是不会抛出异常,而是抛出的异常不是在主线程中,所以对于主线程来说是无感的,但是我们需要知道这个异常,于是就有了error事件。

// worker.js
self.addEventListener('message', (event) => {
    // 抛出异常
    throw new Error('出错了!!!');
});

self.addEventListener('error', (event) => {
    console.log(event.message);
});

上面是在worker.js中抛出异常的例子,我们在worker.js中监听了message 事件,然后在事件处理函数中抛出了一个异常,然后在worker.js中监听了error事件,当worker抛出异常时,就会触发error事件。

在主线程中,我们可以通过Worker对象的onerror属性来监听worker抛出的异常。

// main.js
const worker = new Worker('worker.js');

worker.onerror = (event) => {
    console.log(event.message);
};

在上面的例子中,我们在主线程中创建了一个Worker对象,然后监听了worker抛出的异常。

messageerror 事件

除了上面的message事件和error事件之外,web worker还有一个messageerror事件,同样它也同时存在于主线程和worker中。

它的作用是当传递的数据无法被序列化,那么就会触发messageerror事件。

注意了,它和error事件不一样,error事件是当worker抛出异常时触发的,而messageerror事件是当传递的数据无法被序列化时触发的。

  • worker.js
// worker.js
self.addEventListener('message', (event) => {
    // 向主线程传递数据
    self.postMessage('收到了!!!');
});

self.addEventListener('messageerror', (event) => {
    console.log(event.message);
});
  • main.js
// main.js
const worker = new Worker('worker.js');

worker.postMessage({
    func: () => {
    }
})

worker.onmessageerror = (event) => {
    console.log(event.message);
};

上面的例子中主线程向worker传递了一个对象,但是对象中有一个函数,函数是无法被序列化的,所以会触发messageerror事件。

上面只会触发主线程的messageerror事件,但是不会触发error事件。

worker中的messageerror事件和主线程中的messageerror事件也是同理,worker 如果传递了无法被序列化的数据,那么就会触发workermessageerror事件。

5. 关闭worker

关闭web worker指的是关闭worker线程,就简简单单的停止worker线程的运行,让worker线程不会有任何反应机会。

关闭了的worker是无法再次启动的,如果想要再次启动,那么就需要重新创建一个worker,没有起死回生的机会。

web worker中,我们可以通过close方法来关闭worker

// worker.js
self.addEventListener('message', (event) => {
    // 关闭worker
    self.close();
});

在上面的例子中,我们在worker.js中监听了message事件,然后在事件处理函数中关闭了worker

在主线程中,我们可以通过Worker对象的terminate方法来关闭worker

// main.js
const worker = new Worker('worker.js');

worker.terminate();

在上面的例子中,我们在主线程中创建了一个Worker对象,然后调用了terminate方法来关闭worker

6. worker线程限制

在文章开头我们提到了,web worker是运行在另一个线程中的,这个线程是独立于主线程的,它无法操作主线程的DOM

除了这个限制之外,看上面的描述,它是独立于主线程的,所以它无法访问主线程的任何东西,包括全局变量。

就是因为有了这么些限制,所以web worker才能够在不影响主线程的情况下运行,也就是说web worker 是线程安全的,不像其他的多线程编程,还需要考虑线程安全的问题。

7. worker的实用场景

web worker的出现,然后我们拥有了一个可以发挥多线程能力的工具,那么它有什么实用的场景呢?

很多时候我们会遇到一些耗时的操作,比如说一些复杂的计算,或者是一些网络请求,这些操作都会阻塞主线程,导致页面卡顿,用户体验不好。

这个时候我们就可以把这些耗时的操作放到worker中去执行,这样就不会阻塞主线程了,用户体验会好很多。

就拿网上传烂了的例子,前端一次性渲染十万条数据来说,网上的示例优化的都是DOM 的渲染,但是这个优化对于数据的处理是没有任何帮助的,因为数据的处理是在主线程中执行的,所以还是会阻塞主线程。

例如你有十万条数据,用户怎么可能看的完?肯定是需要有查询筛选的功能,可想而知这个筛选的过程是有多么的耗时,如果是在主线程中执行,那么势必会阻塞主线程,导致页面卡顿。

这个时候我们就可以把数据的处理放到worker中去执行,这样就不会阻塞主线程了,用户体验会好很多。

看示例:

  • main.js
// main.js
const worker = new Worker('worker.js');

const params = {
    name: '',
    age: ''
}
worker.postMessage({search: params});

worker.onmessage = (event) => {
    renderData(event.data);
};

const renderData = (data) => {
    // 渲染数据,这里就是网上说的虚拟滚动的实现
};
  • worker.js
// worker.js
const loadData = () => {
    // 加载数据
    ajax({
        url: 'http://xxx.com',
        success: (data) => {
            self.postMessage(data);
        }
    });
};

const getData = (search) => {
    // 处理数据,肯定是需要循环 10w 次的
    for (let i = 0; i < 100000; i++) {
        // 这里就是处理数据的逻辑
    }
};

self.addEventListener('onmessage', (event) => {
    const {search} = event.data;
    const data = getData(search);
    self.postMessage(data);
});

上面就是一个优化的案例,可以将worker中的代码放到主线程中,对比一下效果,同时也建议大家可以自己写一个简单的例子,体验一下。

8. 总结

总体来说web worker还是比较简单的,上面介绍Worker对象:

Worker对象,只有一个构造函数,两个方法,三个监听事件:

  1. 一个构造函数:Worker()
    • 用来创建一个worker对象
  2. 两个方法:
    • postMessage():用来向worker发送消息
    • terminate():用来终止worker线程
  3. 三个监听事件:
    • onmessage:用来监听worker发送的消息
    • onerror:用来监听worker线程的错误
    • onmessageerror:用来监听worker发送的消息的错误

Worker对象文件中,自带一个slef对象,可以用来监听主线程发送的消息,也可以用来向主线程发送消息:

  1. self.addEventListener([eventName], (event) => {}):用来监听主线程发送的消息
    • eventName:监听的事件名称
      • message:用来监听主线程发送的消息
      • error:用来监听主线程发送的错误
      • messageerror:用来监听主线程发送的消息的错误
    • event:事件对象
      • data:主线程发送的数据
  2. self.postMessage():用来向主线程发送消息
  3. self.close():用来关闭worker线程

真香预告:

  1. Worker中还可以创建多个Worker,打开多线程编程的大门。
  2. ServiceWorker让你的网页拥抱服务端的能力。
  3. SharedWorker让你多个页面相互通信。
  4. 点个赞才有后面的...(我不是骗赞,是后面的内容一下没想好)

今天的内容就到这里,今天是掘金日新计划的最后一天,如果觉得我写的可以,就斗胆向大家讨个赞,点开我的主页,给我的文章点个赞吧,我缺个键盘