前端多线程在开发中的应用总结
应该都知道 JavaScript 本身是单线程的,也就是说在一个给定的时间内,JavaScript 只能执行一个任务。然而HTML5 引入了 Web Workers API,使得 JavaScript 可以创建多个线程(也就是 workers)来并行处理任务。
Web Workers
在主线程中创建一个 Worker 时,浏览器会为其开辟一个新的线程。新的 Worker 线程独立于主线程,有自己的全局上下文,互不干扰,这样就可以在 Worker 线程中执行复杂的、耗时的代码,而不会阻塞主线程,从而提高页面的性能和响应速度。
关于 Web Workers 的主要特点:
-
并行执行:Web Workers 允许在后台线程中并行执行 JavaScript 代码,这样主线程就可以专注于用户交互和页面渲染,而不会被复杂的计算任务阻塞。
-
独立的运行环境:每个 Worker 都运行在自己的全局上下文中,这个上下文与主线程的上下文完全独立。
-
基于消息传递:主线程与 Worker 线程之间的通信是通过消息传递实现的。主线程和 Worker 线程不能直接访问彼此的全局变量,只能通过
postMessage
方法发送消息,通过onmessage
事件接收消息。 -
限制:由于 Worker 线程与主线程是完全独立的,因此在 Worker 线程中有一些限制,例如不能直接操作 DOM,不能访问 window 和 document 对象,不能使用某些默认方法和属性等。
-
Web Workers的线程数上限:上限主要取决于浏览器和硬件的限制。大多数现代浏览器都会对 Web Workers 的数量进行限制,以防止过多的线程消耗过多的系统资源。这个限制通常是动态的,取决于系统的可用资源。例如,Chrome 浏览器的 Web Workers 数量上限大约是每个域名120个。即使浏览器允许创建更多的 Web Workers,硬件资源(如CPU核心数)也可能会成为限制因素。如果创建的 Web Workers 数量超过CPU的核心数,那么这些 Web Workers 将需要在CPU核心之间进行切换,这可能会导致上下文切换的开销,降低性能。因此,虽然 Web Workers 可以帮助在后台线程中运行任务,但是仍然需要谨慎地使用它们,避免创建过多的 Web Workers。在大多数情况下,应该根据任务的性质和硬件的限制,合理地选择 Web Workers 的数量。
-
浏览器兼容性:
一个简单的 Web Workers 的示例
// 在主线程中创建一个 Worker
let worker = new Worker('worker.js');
// 向 Worker 发送消息
worker.postMessage('Hello, worker!');
// 接收 Worker 的消息
worker.onmessage = function(event) {
console.log('Received message from worker: ' + event.data);
};
// 在 worker.js 中
self.onmessage = function(event) {
console.log('Received message from main thread: ' + event.data);
self.postMessage('Hello, main thread!');
};
例子中,在主线程中创建了一个 Worker,然后通过 postMessage
方法向 Worker 发送消息,通过 onmessage
事件接收 Worker 的消息。在 Worker 线程中,也通过 self.onmessage
事件接收主线程的消息,通过 self.postMessage
方法向主线程发送消息。
主线程中和 Worker 线程都可以使用postMessage
方法 和 onmessage
事件。
Worker构造函数
Worker
对象本身并没有静态方法。它的主要方法和事件包括
- postMessage():向
Worker
线程发送消息。 - terminate():立即终止
Worker
线程。 - onmessage:事件处理器,当
Worker
线程发送消息时触发。 - onerror:事件处理器,当
Worker
线程中发生错误时触发。
Worker
构造函数接受一个必需的参数,即要在Worker
线程中运行的脚本的URL。此外,它还可以接受一个可选的参数,这是一个选项对象,可以包含以下属性:
- type:字符串,表示
Worker
的类型。可能的值包括"classic"
(默认值)和"module"
。如果设置为"module"
,则Worker
将被当作ES6模块来加载,并且可以使用import
和export
语句。 - credentials:字符串,表示加载
Worker
脚本时的凭证模式。可能的值包括"omit"
、"same-origin"
和"include"
。默认值是"same-origin"
。
一个使用选项对象的例子
// 创建一个新的 Worker 线程,将其类型设置为 "module"
let myWorker = new Worker('worker.js', { type: 'module' });
例子中,创建了一个新的Worker
线程,这个线程会作为ES6模块来加载worker.js
脚本。
importScripts()
在 Web Worker 中,可以使用 importScripts()
函数来引入一个或多个 JavaScript 脚本。这个函数接受一个或多个字符串参数,每个参数都是一个脚本的 URL。这些脚本会被同步加载,然后按照指定的顺序执行。
// 在 worker.js 中
importScripts('script1.js', 'script2.js');
// 然后就可以使用 script1.js 和 script2.js 中定义的函数和变量了
需要注意的是,importScripts()
是同步的,也就是说,它会阻塞 Worker 线程,直到所有的脚本都加载和执行完毕。因此,应该尽量减少使用 importScripts()
的次数,以避免不必要的阻塞。如果可能,应该一次性引入所有需要的脚本。
此外,由于同源策略的限制,只能引入与 Worker 脚本同源的脚本,除非这个脚本的服务器设置了适当的 CORS 头部。
self
在 Web Worker 中,self
是一个全局对象,它代表了 Worker 本身。可以使用 self
来访问和操作 Worker 的全局上下文。
- 发送和接收消息:可以使用
self.postMessage()
来向主线程发送消息,使用self.onmessage
来接收主线程的消息。
self.onmessage = function(event) {
var data = event.data;
// 处理数据...
self.postMessage(result);
};
- 引入脚本:可以使用
self.importScripts()
来引入一个或多个脚本。
self.importScripts('script1.js', 'script2.js');
- 关闭 Worker:可以使用
self.close()
来立即终止 Worker。
self.close();
前端多线程应用
复杂计算
网页需要进行大量的计算,比如图像处理、大数据分析等,这些计算可能会占用大量的CPU资源,导致用户界面卡顿。这时可以创建一个 Worker,在 Worker 中进行计算,这样就不会阻塞用户界面
假设有一个大的图像文件,需要对每个像素进行一些复杂的计算来应用一个滤镜。可以创建一个 Worker,在 Worker 中进行这种计算
// 在主线程中
var worker = new Worker('image-processing.js');
worker.postMessage(imageData);
worker.onmessage = function(event) {
context.putImageData(event.data, 0, 0);
};
// 在 image-processing.js 中
self.onmessage = function(event) {
var imageData = event.data;
// 对 imageData 进行处理...
self.postMessage(imageData);
};
网络请求
可以在 Worker 中进行网络请求,这样就不会阻塞用户界面。特别是需要进行大量的网络请求,或者需要处理大量的数据时,使用 Worker 会非常有用
// 在主线程中
var worker = new Worker('fetch-data.js');
worker.postMessage(urls);
worker.onmessage = function(event) {
console.log('Received data: ' + event.data);
};
// 在 fetch-data.js 中
self.onmessage = function(event) {
var urls = event.data;
Promise.all(urls.map(url => fetch(url).then(response => response.json())))
.then(data => self.postMessage(data));
};
定时任务
如果网页需要执行一些定时任务,比如每隔一段时间就需要更新一次数据,可以在 Worker 中使用setInterval
或者setTimeout
来执行这些任务。
假设需要每隔一分钟就检查一次新的邮件。可以创建一个 Worker,在 Worker 中使用setInterval
来执行这个任务
// 在主线程中
var worker = new Worker('check-email.js');
worker.postMessage('john@example.com');
worker.onmessage = function(event) {
console.log('New email: ' + event.data);
};
// 在 check-email.js 中
self.onmessage = function(event) {
var email = event.data;
setInterval(function() {
fetch('https://api.example.com/emails?to=' + email)
.then(response => response.json())
.then(data => self.postMessage(data));
}, 60000);
};
实际上,可以在任何需要在后台运行的任务中使用 Web Workers。只要记住, Worker 是运行在单独的线程中,不能访问 DOM,不能访问全局变量,只能通过消息传递来与主线程通信。
多线程一定比单线程速度快吗
多线程一定比单线程速度快吗?当然是不一定。
多线程的速度取决于很多因素,包括任务的性质、线程的数量、线程间通信的开销等。
举一个简单的例子,来说明多线程并不一定比单线程快
// 单线程计算斐波那契数列
function fibonacci(n) {
return n < 2 ? n : fibonacci(n - 1) + fibonacci(n - 2);
}
console.time('Single-threaded Fibonacci');
console.log(fibonacci(40));
console.timeEnd('Single-threaded Fibonacci');
// 多线程计算斐波那契数列
var worker = new Worker(URL.createObjectURL(new Blob([`
self.onmessage = function(event) {
var n = event.data;
postMessage(fibonacci(n));
};
function fibonacci(n) {
return n < 2 ? n : fibonacci(n - 1) + fibonacci(n - 2);
}
`], { type: 'text/javascript' })));
worker.onmessage = function(event) {
console.log(event.data);
console.timeEnd('Multi-threaded Fibonacci');
};
console.time('Multi-threaded Fibonacci');
worker.postMessage(40);
例子中,分别使用单线程和多线程计算斐波那契数列的第40项。会发现,多线程版本并不比单线程版本快,甚至可能更慢。
这是因为创建 worker、发送和接收消息都需要时间。而且由于 JavaScript 的单线程特性,worker 之间不能共享内存,每次通信都需要复制数据,这也会带来额外的开销。因此,对于这种计算密集型的任务,多线程并不会带来性能的提升。
总的来说,是否使用多线程,以及如何使用多线程,需要根据具体的应用场景和需求来决定。
对于 IO 密集型的任务,或者可以并行处理的大数据任务,多线程可能会有很大的帮助。但是对于 CPU 密集型的任务,特别是那些无法并行处理的任务,多线程可能并不会带来性能的提升,甚至可能会降低性能。
总结一下
- HTML5 引入了 Web Workers API,使得 JavaScript 可以创建多个线程(也就是 workers)来并行处理任务
- Web Workers 的主要特点:并行执行、独立的运行环境、基于消息传递、限制、Web Workers 的线程数上限
Worker
对象本身并没有静态方法。它的主要方法和事件包括- postMessage():向
Worker
线程发送消息。 - terminate():立即终止
Worker
线程。 - onmessage:事件处理器,当
Worker
线程发送消息时触发。 - onerror:事件处理器,当
Worker
线程中发生错误时触发。
- postMessage():向
- 注意 Worker 还有一个可选参数
- 主线程和 Worker 线程都通过
postMessage
方法向彼此发送消息,通过onmessage
事件接收的消息 - Worker 中
importScripts()
的使用:引入脚本,受到同源策略限制 - Worker 中使用
self
,self
代表 Worker 本身 - Worker 可以用在复杂计算、网络请求、定时任务中
- 多线程不一定比单线程快
- 浏览器依然对 Web Workers 并发数有限制,所以在单域名下6个并发请求可能是某种需求的速度的上限了
看看负责的项目哪里可以用 Web Workers 优化。
本文完。
参考文献
转载自:https://juejin.cn/post/7281196961751695421