让Web Worker来给你的网页提提速
前言
大家好,我是辉夜真是太可爱啦 。
我们都知道,现在的 CPU 都是多核的,性能都很高。
但是, JS 语言采用的是单线程模型。至于原因可以查阅 单线程的JS 。
也就是说,所有任务只能在一个线程上完成,一次只能做一件事。前面的任务没做完,后面的任务只能等着,根本无法发挥现在多核 CPU 的优势。
如何另起线程
如果我们想在 JS 单线程的基础上,额外创建一个子进程,那么,我们就需要用到 Web Worker
。
在主线程运行的同时,创建一个 Worker 子线程在后台运行,子线程可以执行任务而不干扰用户界面,待任务完成之后,将结果返回给主线程。
例如我们可以将复杂的运算放在 Worker 子线程中运行,子线程和主线程同时工作,待任务完成之后,将结果返回给主线程,主线程也不会因此而堵塞。
Worker 线程一旦新建成功,就会始终运行,不会被主线程上的活动(比如用户点击按钮、提交表单)打断。这样有利于随时响应主线程的通信。但是,这也造成了 Worker 比较耗费资源,不应该过度使用,而且一旦使用完毕,就应该关闭。
tips:异步任务就没必要放 Worker 子线程中运行了,因为按照 JS 运行机制,异步任务本身就不阻塞主线程的执行,等异步任务执行完之后,等主线程空闲之后,再依次执行异步任务队列。具体的执行机制可以查阅我这一篇文章 一文搞懂JS系列(六)之微任务与宏任务,Event Loop
局限性
1. 同源策略
这是由于浏览器的安全机制所导致的,分配给 Worker 线程运行的脚本文件,必须与主线程的脚本文件同源。
2. DOM 限制
由于 Worker 是运行在另一个全局上下文中,不同于当前的 window
,所以,无法读取主线程所在网页的 DOM 对象,也无法使用 document
、 window
、 parent
这些对象。但是,Worker 线程可以navigator
对象和location
对象,以及 WebSockets
,IndexedDB
等功能。
3. 直接通信限制
由于 Worker 线程和主线程不在同一个上下文环境,它们不能直接通信,必须通过消息完成。
双方都使用 postMessage()
方法发送各自的消息,使用 onmessage
事件处理函数来响应消息,这个过程中数据并不是被共享而是被复制。
4. 脚本限制
Worker 线程不能执行 alert()
方法和 confirm()
方法,但可以使用 XMLHttpRequest
, Promise
, Console
等特性。(具体可以查阅 这里)
5. 文件限制
Worker 线程无法读取本地文件,即不能打开本机的文件系统(file://
),它所加载的脚本,必须来自网络。
基本用法
image.png image.png特性检测
在开始使用之前,最好先进行兼容性的判断
if (window.Worker) {
... // do something
}
创建实例
创建一个新的 Worker 很简单,你也可以叫他 专用 Worker 。你需要做的是调用 Worker()
构造函数,指定一个来自网络的脚本。(切记无法读取本地文件)
let myWorker = new Worker('worker.js');
通信联系
主线程
发送数据
主线程调用 myWorker.postMessage()
向子线程发送信息。
myWorker.postMessage('Cool Dream');
myWorker.postMessage({name: 'huiYe', age: 26});
postMessage()
内的参数,就是要发送的数据,它可以是各种数据类型,包括二进制数据。
接收数据
主线程通过worker.onmessage
指定监听函数,接收子线程发回来的消息。
myWorker.onmessage=(res)=>{
console.log(res.data)
}
通过 res.data
可以获取到 Worker 子线程中发过来的数据。
Worker 子线程
在 Worker 子线程中,有一个 self
关键字,代表子线程本身。
可以通过 self.name
获取当前的子线程名字。
发送数据
self.postMessage('Cool Dream');
self.postMessage({name: 'huiYe', age: 26});
接收数据
主线程通过worker.onmessage
指定监听函数,接收子线程发回来的消息。
self.onmessage=(res)=>{
console.log(res.data)
}
通过 res.data
可以获取到主线程中发过来的数据。
也可以通过 self.addEventListener()
来获取数据。
self.addEventListener('message', res=>{
console.log(res.data)
}, false);
错误处理
当 Worker 发生错误时,可以在主线程中通过 onerror
或者 addEventListener('error')
监听到
myWorker.onerror((error)=>{
console.log(error)
});
// 或者
myWorker.addEventListener('error', ()=>{
// ...
});
资源关闭
使用完之后,一定要记得关闭它,避免多余的资源浪费
// 主线程
myWorker.terminate();
// Worker 线程
self.close();
进阶
Worker 加载脚本
有的时候,我们需要在 Worker 中加载一个或多个脚本,那么,就需要用到 importScripts
// 加载一个脚本
importScripts('script1.js');
// 加载多个脚本
importScripts('script1.js', 'script2.js');
浏览器加载并运行每一个 importScripts
引入的脚本。
每个脚本中的全局对象都能够被 Worker 使用。如果脚本无法加载,将抛出 NETWORK_ERROR
异常,接下来的代码也无法执行。而之前执行的代码(包括使用 window.setTimeout()
异步执行的代码)依然能够运行。
importScripts()
之后的函数声明依然会被保留,由于变量提升的缘故,所以它们始终会在其他代码之前运行。
可转让对象
在上面,我们提到了主线程和子线程的通信过程中,使用的 postMessage
可以传递各种类型的数据。
而这种通信的方式,是拷贝关系。当传递的值是引用类型的时候,传递的也是对象的值,而不是引用地址。所以,在 Worker 中对传递的内容进行修改也不会造成主线程的数据修改。
传递对象的值,当传递的对象足够庞大时,就会引发一个性能问题。
这种时候,可以使用 可转让对象 ,相当于让这个对象的引用从当前执行上下文转移到另一个执行上下文,当前执行上下文中的这个对象就会完全无法使用,有点资产转移的味道了。
Worker 中创建 Worker
对,你没听错,Worker 线程内部还能再新建 Worker 线程。