🎉🎉🎉 Web Workers 使用秘籍,祝您早日通关前端多线程!
Web Workers 是新一代的异步编程解决方案,它可以让我们在后台运行一个脚本,而不会阻塞用户界面。
对于前端开发者来说,Web Workers 是一个非常有用的工具,它可以让我们在后台运行一些耗时的任务,比如计算、数据处理等,而不会阻塞用户界面。
接下来就带你正式上手 Web Workers。
开始之前的准备工作
根据评论区的小伙伴的需求,特地补上这一说明。
Web Workers是需要运行在服务环境中(http/https协议),也就是如果我们通过本地直接预览html是不行的(file协议),这个时候解决方案有很多,最简单的解决方案是通过 ide,下面就介绍各种解决方案。
WebStorm 用户
我就是WebStorm的用户,可以直接在html文件中右键点击,然后选择运行 or 调试都可以。

vscode 用户
vscode可以在vscode中安装Live Server插件;
安装成功后,用vscode打开html文件所在的文件夹
在vscode中直接右击 Open with Live Server打开即可!
不想装插件?
不想装插件就麻烦一些了:
- 可以直接下载
tomcat或者nginx在自己的电脑上面跑一个服务。 - 可以通过
http-server来开启一个服务,npm install http-server -g。 - 使用
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. 传递数据
上面的示例中,我们只是传递了一个字符串,但是实际上,我们可以传递任何数据类型,比如ArrayBuffer、Blob、MessagePort等。
我们来看一个例子:
- 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对象,比如String
、Number、Boolean、Array、Object等。
是的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
如果传递了无法被序列化的数据,那么就会触发worker的messageerror事件。
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对象,只有一个构造函数,两个方法,三个监听事件:
- 一个构造函数:
Worker()- 用来创建一个
worker对象
- 用来创建一个
- 两个方法:
postMessage():用来向worker发送消息terminate():用来终止worker线程
- 三个监听事件:
onmessage:用来监听worker发送的消息onerror:用来监听worker线程的错误onmessageerror:用来监听worker发送的消息的错误
Worker对象文件中,自带一个slef对象,可以用来监听主线程发送的消息,也可以用来向主线程发送消息:
self.addEventListener([eventName], (event) => {}):用来监听主线程发送的消息eventName:监听的事件名称message:用来监听主线程发送的消息error:用来监听主线程发送的错误messageerror:用来监听主线程发送的消息的错误
event:事件对象data:主线程发送的数据
self.postMessage():用来向主线程发送消息self.close():用来关闭worker线程
真香预告:
Worker中还可以创建多个Worker,打开多线程编程的大门。ServiceWorker让你的网页拥抱服务端的能力。SharedWorker让你多个页面相互通信。- 点个赞才有后面的...(我不是骗赞,是后面的内容一下没想好)
转载自:https://juejin.cn/post/7160676211780714526