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
让你多个页面相互通信。- 点个赞才有后面的...(我不是骗赞,是后面的内容一下没想好)