上篇讲到了如何使用Worker,介绍了Worker的基本用法,如何创建Worker,如何向Worker发送消息,如何接收Worker的消息,以及如何关闭Worker。
本篇探索Worker的更多用法,主要是如何创建多个Worker,和在Worker中如何发送请求,以及如何使用Worker进行多线程编程。
历史回顾:
创建多个 Worker
在上篇文章中,我们已经介绍了如何创建一个Worker,但是在实际开发中,我们可能需要创建多个Worker,这还不简单嘛,多new几个不就好了。
// main.js
const worker1 = new Worker('worker.js')
const worker2 = new Worker('worker.js')
const worker3 = new Worker('worker.js')
worker1.postMessage('worker1')
worker2.postMessage('worker2')
worker3.postMessage('worker3')
// worker.js
self.addEventListener('message', (e) => {
console.log(e.data)
})
这里的worker.js是可以复用的,但是数据不会共享,每个Worker都是独立的,我们可以创建多个Worker,但是不要创建太多,因为每个Worker都会占用一定的内存。

Worker 中创建 Worker
上面说到可以创建多个Worker,但是这里的Worker是指主线程中的Worker,那么我们可以在Worker中创建Worker吗?答案是肯定的。
在Worker中,我们可以创建多个Worker,创建方法也很简单,就和我们上面讲到的创建Worker一样,使用new Worker()即可。
这里就将创建的Worker当作主线程来理解就好了,主线程中可以创建Worker,Worker中也可以创建Worker。
// worker.js
var worker = new Worker('worker1.js');
worker.postMessage('这里是Worker的主线程发送的消息');
worker.onmessage = (e) => {
console.log(e.data);
}
// worker1.js
self.addEventListener('message', (event) => {
console.log(event.data);
});
self.postMessage('这里是Worker1的子线程发送的消息');

到这里我们已经摸到了Worker的奥妙,也打开了前端多线程的编程大门,接下来了就开始进阶吧。
Worker 中引用外部文件
在Worker中,我们可以引用外部文件,这样有利于我们将代码进行拆分,方便管理,代码如下:
self.importScripts('worker1.js', 'worker2.js', ...);
这里使用了importScripts方法,它的作用是引入外部文件,可以引入多个文件,文件之间用逗号隔开。
它的实际作用是将外部文件的内容拷贝到Worker中,这样就可以在Worker中使用外部文件中的内容了。
我们先来看一下实际效果:
// worker.js
importScripts('worker1.js', 'worker2.js');
worker1();
worker2();
// worker1.js
var worker1 = () => {
console.log('worker1');
}
// worker2.js
var worker2 = () => {
console.log('worker2');
}

可以看到,Worker中引用了外部文件,且可以使用外部文件中的内容。
这样我们的Worker也可以分模块了,不用把所有的代码都写在一个文件中。
importScripts() 方法
importScripts是Worker中的一个全局方法,它的作用是引入外部文件,上面已经体验过了,现在来详解。
importScripts()和self.importScripts()是相等的,他们就是一个东西,在Worker的全局作用域中,都是可以省去self的。
除了可以引入外部文件,还可以引入外部的URL,代码如下:
importScripts('https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.min.js');
但是这样会直接报错,因为这个URL不是同源的,所以使用importScripts()引入外部URL时,必须是同源的。
Worker 中使用 XMLHttpRequest
上面我们已经知道了Worker中可以引入外部文件,现在我们还需要使用外部数据,就是请求后台数据,这时候就需要使用ajax了。
但是上面可以看到我们引用Jquery的时候报错了,那么引用axios啥的肯定也是不行的,但是我们可以使用XMLHttpRequest。
XMLHttpRequest是ajax的基础,它是ajax的核心,我们可以使用它来请求后台数据。
// worker.js
var xhr = new XMLHttpRequest();
xhr.open('GET', 'https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.min.js', true);
xhr.send();
xhr.onreadystatechange = function () {
if (xhr.readyState === 4 && xhr.status === 200) {
console.log(xhr.responseText);
}
}
可以看到,我们使用XMLHttpRequest请求了Jquery的CDN,并且成功拿到了数据。
Worker 中使用 fetch
上面使用XMLHttpRequest请求数据,这个太底层了,不利于我们的开发,这里庆幸的是,Worker全局中提供了fetch方法,我们可以使用它来请求数据。
// worker.js
fetch('https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.min.js')
.then(res => res.text())
.then(data => {
console.log(data);
})
可以看到,我们使用fetch请求了Jquery的CDN,并且成功拿到了数据。
fetch是ajax的新标准,它的优点是:简单、易用、强大,它的缺点是:兼容性不好,不支持IE。
抛开IE其实fetch已经被很广泛的使用了,各大浏览器都已经支持了,所以我们可以放心的使用它。fetch和XMLHttpRequest都是用于发送HTTP请求的,但是fetch的语法更加简洁,而且fetch支持Promise,所以我们可以使用async/await来发送请求。
由于本系列文章是为了学习Web Worker,所以这里就不详细介绍fetch和XMLHttpRequest了,有兴趣的可以自行查阅相关资料。
实践
现在我们来实践一下,还是拿上次的10万数据的例子来,不过这次我们弄到100万数据,然后使用Worker来处理。
// main.js
const worker = new Worker('worker.js');
worker.onmessage = (event) => {
renderData(event.data);
};
const renderData = (data) => {
console.log(data);
// 渲染数据,这里就是网上说的虚拟滚动的实现
};
// 延时进行查询请求,这是为了防止 Worker 把数据发送过来就操作,导致不生效
setTimeout(() => {
const params = {
name: '1',
age: ''
}
worker.postMessage({
search: params});
}, 1000);
// worker.js
const data = [];
const loadData = () => {
// 加载数据
// 这里可以使用fetch请求数据,现在这里还是空着,留着下次完成
for (var i = 0; i < 1000000; i++) {
data.push({
name: 'name' + i,
age: i
});
}
self.postMessage(data);
};
loadData();
self.addEventListener('message', (event) => {
const {
search} = event.data;
shardQuery(search).then((data) => {
self.postMessage(data);
});
});
// 这里分片处理数据,交给多个线程处理
const shardQuery = (search) => {
return new Promise((resolve, reject) => {
// 这里定义一个计数器,确保所有线程都执行完毕
let counter = 0;
const shardData = [];
// 这里每个线程处理10万条数据,最终会有10个线程
const shardSize = 100000;
for (let i = 0; i < data.length; i += shardSize) {
counter += 1;
const filterWorker = new Worker('filterData.js');
filterWorker.postMessage({
data: data.slice(i, i + shardSize), search});
filterWorker.onmessage = (event) => {
shardData.push(...event.data);
counter -= 1;
// 所有线程都执行完毕,返回结果
if (counter === 0) {
resolve(shardData);
}
};
}
});
};
// filterData.js
const filterData = (data, search) => {
if (!data || data.length === 0) {
return [];
}
const {
name, age} = search;
const result = [];
for (var i = 0; i < data.length; i++) {
const item = data[i];
let flag = true;
if (search.name && item.name.indexOf(name) === -1) {
flag = false;
}
if (search.age && item.age !== age) {
flag = false;
}
if (flag) {
result.push(item);
}
}
return result;
};
self.addEventListener('message', (event) => {
const {
data, search} = event.data;
const result = filterData(data, search);
self.postMessage(result);
});
可以看到,我们使用Worker来处理数据,这里我们使用了分片处理数据,交给多个线程处理,这样可以提高处理效率。
注意:
Worker的创建是需要消耗资源的,所以不要创建太多的Worker,这样会导致性能下降;
我这里创建了10个Worker,理论上是有点多的;
同时我这里的Worker也没有销毁,这里其实可以使用线程池的思想来处理。
总结与预告
到这里,我们已经学习完了Worker的相关知识;
第一篇我们知道怎么使用Worker,知道主线程和子线程的通讯,知道了子线程和主线程的通讯;
这一篇我们学习了创建多个Worker,知道了Worker中如何引入外部资源,同时也进一步的了解了Worker中的一些限制。
接下来我们会认识新的Worker,SeviceWorker,这个Worker可以让我们在离线的情况下也能访问网站,可以实现缓存,实现消息推送等等,让我们一起来期待吧。