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 |

注意:
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 URI -
options目标类型,然后有三个可选的特点值,分别是type,credentials以及name -
options.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内履行上下文抛出错误时,会触发当时事情
总结:
尽管 Worker 线程是在浏览器环境中被引发,可是它与当时页面窗口运行在不同的大局上下文中,咱们常用的顶层目标 window,以及 parent 目标在 Worker 线程上下文中是不可用的。它有自己的顶层目标,即self。别的,在 Worker 线程上下文中,操作 DOM 的行为也是不可行的,document目标也不存在。可是,location和navigator目标能够以可读办法拜访。除此之外,绝大多数 Window 目标上的办法和特点,都被同享到 Worker 上下文大局目标 WorkerGlobalScope 中。
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导出
以上列举了3个场景,希望能带给咱们一些启示,结合自己的事务场景考虑要不要运用。当然这么纯剖析比较笼统,下面让咱们结合详细的demo感受一下worker的魅力。当然也不是任何场景都适合worker的,那么什么场景下不适用呢,由于篇幅原因,详细请移步到下一篇文章-提高用户体会计划之Web Worker—Worker2。




