上篇讲到了如何使用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
可以让我们在离线的情况下也能访问网站,可以实现缓存,实现消息推送等等,让我们一起来期待吧。