提升用户体验方案之Web Worker—Worker1
1. 什么是WebWorker
众所周知,JavaScript 是单线程的语言。所有代码都运行在一个主线程中,包括处理用户界面,js代码执行和网络请求。当执行耗时操作时,就会导致用户页面的卡顿和不响应,甚至浏览器直接卡死。现在前端遇到大量计算的场景越来越多,为了有更好的体验,HTML5 中提出了 Web Worker 的概念。
知识点回顾:为什么javaScript是单线程语言,多个线程不是更能提高效率吗?
JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,
JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?
Web Worker 是一种在浏览器中运行的 JavaScript 脚本,可以在独立的线程中执行,与主线程并行工作,提供了一种在后台执行复杂计算或处理耗时操作的方式,而不会堵塞主线程的执行。它还可以与主线程进行通信,通过信息传递机制来交换数据和结果,从而形成了高效、良好的用户体验。
Web Worker 是一个统称,具体可以细分为普通的 Worker、SharedWorker 和 ServiceWorker 等。
3种worker分别适合不同的场景,普通的 Worker 可以在需要大量计算的时候使用,创建新的线程可以降低主线程的计算压力,不会导致 UI 卡顿。SharedWorker 主要是为不同的 window、iframes 之间共享数据提供了另外一个解决方案。ServiceWorker 可以缓存资源,提供离线服务或者是网络优化,加快 Web 应用的开启速度。
下面用一个表格大概了解一下三种worker的不同,本章重点介绍Worker相关内容。
| 类型 | Worker | SharedWorker | ServiceWorker |
|---|---|---|---|
| 通信方式 | postMessage | port.postMessage | 单向通信,通过 addEventListener 监听 serviceWorker 的状态 |
| 使用场景 | 适合大量计算的场景 | 适合跨 tab、iframes 之间共享数据 | 缓存资源、网络优化 |
| 兼容性 | >= IE 10 >= Chrome 4 | 不支持 IE、Safari、Android、iOS >= Chrome 4 | 不支持 IE >= Chrome 40 |
web workers 已经被大多数浏览器支持,使用上基本不用考虑兼容问题。
注意:
worker的实际上是一个较重的API,它对浏览器是有一定的负担的,所以建议需要认真分析当前的场景是否是需要使用worker进行业务的实现或者性能的优化。
2. worker
worker使用Worker(...)构造来生成一个worker实例对象,他的定义主要如下
[Exposed=(Window,DedicatedWorker,SharedWorker)]
interface Worker : EventTarget {
constructor(USVString scriptURL, optional WorkerOptions options = {});
undefined terminate();
undefined postMessage(any message, sequence<object> transfer);
undefined postMessage(any message, optional StructuredSerializeOptions options = {});
attribute EventHandler onmessage;
attribute EventHandler onmessageerror;
};
dictionary WorkerOptions {
WorkerType type = "classic";
RequestCredentials credentials = "same-origin"; // credentials is only used if type is "module"
DOMString name = "";
};
enum RequestCredentials { "omit", "same-origin", "include" };
enum WorkerType { "classic", "module" };
Worker includes AbstractWorker;
2.1 构造函数
我们可以看到worker继承于EventTarget,它本身就是采用的浏览器事件接口的。除此之外,构造函数接受两个参数分别是scriptURL以及options
scriptURL用于指定要加载的worker模块的脚本地址,部分浏览器环境支持使用data URIoptions对象类型,然后有三个可选的属性值,分别是type,credentials以及nameoptions.type选择加载类型,可以使用的值有classic以及module,使用module允许使用ES Module对文件进行处理,支持模块导入和导出,提供了更好的模块化支持options.credentials用于制定加载时文件时,是否携带cookie等身份认证信息,点击查看详细介绍options.nameworker的名称,据MDN上说一般用于调试
2.2 Worker的全局对象
首先我们需要知道一下DedicatedWorkerGlobalScope的继承关系。

首先DedicatedWorkerGlobalScope继承于EventTarget和WorkerGlobalScope。可以使用父类的方法。实际上worker也就是一个事件目标对象(EventTarget),它也可以挂载EventListener进行事件监听。那么在了解完继承关系后我们再来梳理一下WorkerGlobalScope提供了什么方法以及属性
EventTarget是事件目标接口,用于处理事件。WorkerGlobalScope继承了这个接口,使得 Worker 线程能够处理事件,例如onmessage和onerror。
WorkerGlobalScope
WorkerGlobalScope 是 Web Workers 中的全局对象,类似于浏览器中的 window 对象。在这个全局作用域中,可以执行 JavaScript 代码,但是它没有直接访问 DOM 的能力,因为 DOM 是主线程的一部分。
WorkerGlobalScope继承了EventTarget,并且实现了一些其他的接口,包括WindowTimers,WindowBase64,WindowEventHandlers 和 GlobalFetch 接口
WindowTimers接口:WindowTimers定义了在定时器方面的方法,如setTimeout和setInterval。在 Worker 线程中,由于没有 DOM,定时器方法的实现会有所不同,但仍然提供了类似的功能。WindowTimers.clearInterval()、WindowTimers.clearTimeout()、WindowTimers.setInterval()、WindowTimers.setTimeout()WindowBase64接口:WindowBase64提供了一些用于处理 Base64 编码的方法。在 Worker 线程中,这样的方法仍然可以用于处理数据。WindowBase64.atob()/WindowBase64.btoa()WindowEventHandlers接口:WindowEventHandlers定义了处理事件的方法。虽然 Worker 线程无法直接与 DOM 交互,但它仍然可以处理一些与事件相关的操作。GlobalFetch接口:GlobalFetch提供了在全局范围内进行网络请求的方法,例如fetch。这允许 Worker 线程进行网络通信,获取数据等。GlobalFetch.fetch()
这些接口的继承和实现使得 WorkerGlobalScope 具有一些全局作用域应该具备的通用特性,同时也适应了 Web Worker 的环境。请注意,在 Worker 线程中,并不是所有的 Window 对象的属性和方法都会被实现,因为 Worker 线程中没有 DOM。sessionStorage和localStorage也是没有办法在WorkerGlobalScope中使用的。在worker中可以使用的浏览器存储有IndexedDB。
它拥有以下属性以及方法:
-
WorkerGlobalScope.caches(只读对象): 返回与当前上下文相关的CacheStorage对象,它主要与缓存相关,一般用于service worker中。 -
WorkerGlobalScope.navigator(只读对象): 返回与worker关联的WorkerNavigator它是一个特定的导航器对象,适用worker。 -
WorkerGlobalScope.self(只读对象): 返回对 WorkerGlobalScope 本身的引用。大多数情况下,它是一个特定的范围,例如 DedicatedWorkerGlobalScope、SharedWorkerGlobalScope (en-US) 或 ServiceWorkerGlobalScope。 -
WorkerGlobalScope.location(只读对象): 返回与worker关联的WorkerLocation,Worker 线程的位置信息。与浏览器的主线程不同,Worker 线程中的location对象是只读的,且只包含href属性,适用于worker。 -
WorkerGlobalScope.onerror: 用于设置或获取在 Worker 线程中捕获全局错误的事件处理函数。 -
WorkerGlobalScope.close()丢弃在 WorkerGlobalScope 的事件循环中排队的任何任务,关闭当前作用域,在 Worker 线程中调用这个方法将会终止该线程 -
WorkerGlobalScope.importScripts()可以动态将多个脚本引入当前worker的上下文中 -
通过其他接口实现的方法
DedicatedWorkerGlobalScope
DedicatedWorkerGlobalScope 接口表示 Dedicated Worker(专用 Worker)中的全局作用域。这个接口继承自 WorkerGlobalScope,所以包括了与 WorkerGlobalScope 相关的属性和方法。以下是 DedicatedWorkerGlobalScope 特有的属性和方法:
DedicatedWorkerGlobalScope.postMessage(): Dedicated Worker 全局作用域中的 postMessage 方法,用于向主线程发送消息。该方法可以传递多种类型的message的给到外部的worker实例,通过message事件进行监听。
2.3 worker实例对象
了解完worker内部的全局对象后,我们再来了解一下worker实例。Worker在上文提到过,也是继承于EventTarget所以具备事件目标的相关属性以及方法。除此以外它还具备以下方法以及事件
-
Worker.postMessage()可以用于跟worker内部的上下文进行通讯,同DedicatedWorkerGlobalScope.postMessage方法参数 -
Worker.terminate()结束当前worker的行为,不会等待worker完成剩余的操作 -
message用于接收来自于worker内部上下文的message。 -
messageerror同DedicatedWorkerGlobalScope的messageerror事件,当worker内给外部实例传递一条无法返序列化的数据是有此报错 -
error当在worker内执行上下文抛出错误时,会触发当前事件
3. worker的使用
3.1 创建 worker
创建 worker 只需要通过 new 调用 Worker() 构造函数即可,它接收两个参数
const worker = new Worker(path, options);
| 参数 | 说明 |
|---|---|
| path | 有效的js脚本的地址,必须遵守同源策略。无效的js地址或者违反同源策略,会抛出SECURITY_ERR 类型错误 |
| options.type | 可选,用以指定 worker 类型。该值可以是 classic 或 module。 如未指定,将使用默认值 classic |
| options.credentials | 可选,用以指定 worker 凭证。该值可以是 omit, same-origin,或 include。如果未指定,或者 type 是 classic,将使用默认值 omit (不要求凭证) |
| options.name | 可选,在 DedicatedWorkerGlobalScope的情况下,用来表示 worker 的 scope 的一个 DOMString值,主要用于调试目的。 |
3.2 主线程与 worker 线程数据传递
主线程与 worker 线程都是通过 postMessage 方法来发送消息,以及监听 message 事件来接收消息。如下所示:
// main.js(主线程)
const myWorker = new Worker('/worker.js'); // 创建worker
myWorker.addEventListener('message', e => { // 接收消息
console.log(e.data); // Greeting from Worker.js,worker线程发送的消息
});
// 这种写法也可以
// myWorker.onmessage = e => { // 接收消息
// console.log(e.data);
// };
myWorker.postMessage('Greeting from Main.js'); // 向 worker 线程发送消息,对应 worker 线程中的 e.data
// worker.js(worker线程)
self.addEventListener('message', e => { // 接收到消息
console.log(e.data); // Greeting from Main.js,主线程发送的消息
self.postMessage('Greeting from Worker.js'); // 向主线程发送消息
});
好了,一个简单 worker 线程就创建成功了。
postMessage() 方法接收的参数可以是字符串、对象、数组等。具体我们在3.7讨论。
主线程与 worker 线程之间的数据传递是传值而不是传地址。所以你会发现,即使你传递的是一个Object,并且被直接传递回来,接收到的也不是原来的那个值了。
// main.js(主线程)
const myWorker = new Worker('/worker.js');
const obj = {name: '小明'};
myWorker.addEventListener('message', e => {
console.log(e.data === obj); // false
});
myWorker.postMessage(obj);
// worker.js(worker线程)
self.addEventListener('message', e => {
self.postMessage(e.data); // 将接收到的数据直接返回
});
3.3 监听错误信息
web worker 提供两个事件监听错误,error 和 messageerror。这两个事件的区别是:
| 事件 | 描述 |
|---|---|
error | 当worker内部出现错误时触发 |
messageerror | 当 message 事件接收到无法被反序列化的参数时触发 |
监听方式跟接收消息一致:
// main.js(主线程)
const myWorker = new Worker('/worker.js'); // 创建worker
myWorker.addEventListener('error', err => {
console.log(err.message);
});
myWorker.addEventListener('messageerror', err => {
console.log(err.message)
});
// worker.js(worker线程)
self.addEventListener('error', err => {
console.log(err.message);
});
self.addEventListener('messageerror', err => {
console.log(err.message);
});
3.4 关闭 worker 线程
worker 线程的关闭在主线程和 worker 线程都能进行操作,但对 worker 线程的影响略有不同。
// main.js(主线程)
const myWorker = new Worker('/worker.js'); // 创建worker
myWorker.terminate(); // 关闭worker
// worker.js(worker线程)
self.close(); // 直接执行close方法就ok了
无论是在主线程关闭 worker,还是在 worker 线程内部关闭 worker,worker 线程当前的 Event Loop 中的任务会继续执行。至于 worker 线程下一个 Event Loop 中的任务,则会被直接忽略,不会继续执行。
区别是,在主线程手动关闭 worker,主线程与 worker 线程之间的连接都会被立刻停止,即使 worker 线程当前的 Event Loop 中仍有待执行的任务继续调用 postMessage() 方法,但主线程不会再接收到消息。
在 worker 线程内部关闭 worker,不会直接断开与主线程的连接,而是等 worker 线程当前的 Event Loop 所有任务执行完,再关闭。也就是说,在当前 Event Loop 中继续调用 postMessage() 方法,主线程还是能通过监听message事件收到消息的。
举例说明:
在主线程关闭 worker
大家可以思考一下,主线程会接收到哪些消息呢,控制台会打印出哪些信息呢?
// main.js(主线程)
const myWorker = new Worker('/worker.js'); // 创建 worker
myWorker.addEventListener('message', e => {
console.log(e.data);
myWorker.terminate(); // 关闭 worker
});
myWorker.postMessage('Greeting from Main.js');
// worker.js(worker线程)
self.addEventListener('message', e => {
postMessage('Greeting from Worker');
//settimeput添加一个宏任务
setTimeout(() => {
console.log('setTimeout run');
postMessage('Greeting from SetTimeout');
});
//promise添加一个微任务
Promise.resolve().then(() => {
console.log('Promise run');
postMessage('Greeting from Promise');
})
for (let i = 0; i < 1001; i++) {
if (i === 1000) {
console.log('Loop run');
postMessage('Greeting from Loop');
}
}
});
运行结果如下:

- 主线程只会接收到 worker 线程第一次通过
postMessage()发送的消息,后面的消息不会接收到; - worker 线程当前 Event Loop 里的任务会继续执行,包括微任务;
- worker 线程里 setTimeout 创建的下一个 Event Loop 任务队列没有执行。
在 worker 线程内部关闭 worker
对上述例子稍作修改,将关闭 worker 的事件放到 worker 线程内部,大家觉得又会打印出什么呢
// main.js(主线程)
const myWorker = new Worker('/worker.js'); // 创建 worker
myWorker.addEventListener('message', e => {
console.log(e.data);
});
myWorker.postMessage('Greeting from Main.js');
// worker.js(worker线程)
self.addEventListener('message', e => {
postMessage('Greeting from Worker');
self.close(); // 关闭 worker
setTimeout(() => {
console.log('setTimeout run');
postMessage('Greeting from SetTimeout');
});
Promise.resolve().then(() => {
console.log('Promise run');
postMessage('Greeting from Promise');
})
for (let i = 0; i < 1001; i++) {
if (i === 1000) {
console.log('Loop run');
postMessage('Greeting from Loop');
}
}
});
运行结果如下:

与在主线程关闭不同的是,worker 线程当前的 Event Loop 任务队列中的 postMessage() 事件都会被主线程监听到。
3.5 Worker 线程引用其他js文件
总有一些场景,需要放到 worker 进程去处理的任务很复杂,需要大量的处理逻辑,我们当然不想把所有代码都塞到 worker.js 里,那样就太糟糕了。web worker 为我们提供了解决方案,我们可以在 worker 线程中利用 importScripts() 方法加载我们需要的js文件,而且,通过此方法加载的js文件不受同源策略约束!
// utils.js
const add = (a, b) => a + b;
// worker.js(worker线程)
// 使用方法:importScripts(path1, path2, ...);
importScripts('./utils.js');
console.log(add(1, 2)); // log 3
3.6 ESModule 模式
还有一些场景,当你开启一个新项目,用 importScripts() 导入js文件时发现, importScripts() 方法执行失败。仔细一看,发现是新项目的 js 文件都用的是 ESModule 模式。难道要把引用到的文件都改一遍吗?当然不是,还记得上文提到初始化 worker 时的第二个可选参数吗,我们可以直接使用 module 模式初始化 worker 线程!
// main.js(主线程)
const worker = new Worker('/worker.js', {
type: 'module' // 指定 worker.js 的类型
});
// utils.js
export default add = (a, b) => a + b;
// worker.js(worker线程)
import add from './utils.js'; // 导入外部js
self.addEventListener('message', e => {
postMessage(e.data);
});
add(1, 2); // log 3
export default self; // 只需把顶级对象self暴露出去即可
3.7 主线程和 worker 线程可传递哪些类型数据
很多场景,在调用某些方法时,我们将一些自定义方法当作参数传入。但是,当你使用 postMessage() 方法时这么做,将会导致 DATA_CLONE_ERR 错误。
// main.js(主线程)
const myWorker = new Worker('/worker.js'); // 创建worker
const fun = () => {};
myWorker.postMessage(fun); // Error:Failed to execute 'postMessage' on 'Worker': ()=>{} could not be cloned.
那么,使用 postMessage() 方法传递消息,可以传递哪些数据?
postMessage() 传递的数据可以是由结构化克隆算法处理的任何值或 JavaScript 对象,包括循环引用。
结构化克隆算法不能处理的数据:
-
Error以及Function对象; -
DOM 节点
-
对象的某些特定参数不会被保留
RegExp对象的lastIndex字段不会被保留- 属性描述符,setters 以及 getters(以及其他类似元数据的功能)同样不会被复制。例如,如果一个对象用属性描述符标记为 read-only,它将会被复制为 read-write
- 原形链上的属性也不会被追踪以及复制。
结构化克隆算法支持的数据类型:
- 除去Symbol的所有原始类型:null、undefind、bolean、number、string、bigint
- Boolean 对象
- String 对象
- Date 对象
- RegExp
lastIndex字段不会被保留。 - File
- FileList
- ArrayBuffer
- TypedArray 这基本上意味着所有的 类型化数组 ,如 Int32Array 等。
- ImageData
- Map
- Set
4. woker的实践
前面我们提到了,对于复杂计算和耗时操作,阻塞主线程操作,可以考虑使用woker来解决。让我们一起来结合项目实践思考一下具体的应用场景:
场景1:大文件切片上传
思路分析:
- step1: 将文件切片,根据文件大小和每个要切多大计算切多少片,用于切片的下标计算
- step2 使用FileReader 对象来异步读取文件的分片,
- step3:使用了 SparkMD5 来计算哈希值,以确保文件的完整性。
- step4:每个分片读取完成后,通过 Promise 的 resolve 方法返回一个包含分片信息的对象,包括分片的起始位置、结束位置、索引、计算的哈希值以及分片的文件对象。
- step5:切片完成,拿到所有切片信息,进行上传
那么哪个步骤可以使用worker来处理呢?
没错。对于切片的计算可以放在worker里处理,也就是step2和step3,处理完后将切片信息放到一个数组里通过postmessage传给主线程,主线程通过onmessage可以拿到所有切片信息。主线程终止worker线程。
你甚至可以拿到用户设备的逻辑处理器核心数量,创建多个并行worker,将切片总量均分到每个worker,进行并行计算,当所有线程处理完成后,再进行主线程的下一步处理。这样能缩短处理时间,更进一步的提升用户体验

场景2:用户输入的内容重塑
比如有个需求,给了一系列表单,里面有一个文本框,用户输入大段内容,需要前端根据用户输入的内容进行重新整合,比如样式重绘,识别特殊文本高亮等。
思路分析:
试想对于超大段的内容处理是不是很费时,很容易造成页面卡顿,这个时候就可以考虑用worker了,把用户的输入value传给worker,在worker里进行各种花样解析,处理完后传给主线程进行渲染展示,worker工作时不影响用户操作别的表单项。oh,多么丝滑的体验。
场景3:table导出大文件Excel
表格是我们经常接触的东西,当系统里有table表格的时候,那么它大概率还会伴着导出excel的需求。当我们扛着40米大刀架到后端脖子上,后端表示:要excel没有,要命一条!ok,关键时候还得靠我们前端拯救世界。
思路分析
- step1 :通过
exceljs构建表格相关的参数 - step2:传入相关的数据,然后转换为
blob流, - step3:最后通过
file-save导出
如果你只是这样吭哧吭哧的做了,那么产品经理一定会举着他们80米的大刀来问你做了个什么玩意儿。好的,坚强的前端仔绝不认输,我们来优化一下。
首先创建worker线程,通过postmessage向worker线程传递相应的excel数据,在worker线程中通过exceljs构建表格相关数据,然后转换为blob流,接着将生成的blob流通过postmessage传回来主线程,最后通过file-save导出
转载自:https://juejin.cn/post/7345672631323115570