Web Worker

Web Worker

由来

JS 是单线程。当面临需要大量计算的场景时(如视频解码等),UI 线程就会被阻塞,甚至浏览器卡死。HTML5 中提出了 Web Worker 的概念。

Web Worker 可以使脚本运行在新的线程中,它们独立于主线程,可以进行大量的计算活动,而不会影响主线程的 UI 渲染。当计算结束之后,它们可以把结果发送给主线程,从而形成了高效、良好的用户体验。Web Worker 具体可以细分为普通的 Worker、SharedWorker 和 ServiceWorker 等

普通 Worker

创建 worker

1
const worker = new Worker('./worker.js'); // 参数是url,这个url必须与创建者同源 

worker 的一些方法

onmessage

主线程中可以在 Worker 上添加 onmessage 方法,用于监听 Worker 的信息。

1
2
3
4
const worker = new Worker('./worker.js');
worker.onmessage = function (messageEvent) {
console.log(messageEvent)
}
onmessageerror

主线程中可以在 Worker 上添加 onmessageerror 方法,用于监听 Worker 的错误信息。

1
2
3
4
const worker = new Worker('./worker.js');
worker.onmessageerror = function (messageEvent) {
console.log(messageEvent)
}
postMessage()

主线程通过此方法给 Worker 发送消息,发送参数的格式不限(可以是数组、对象、字符串等),可以根据自己的业务选择。

1
2
const worker = new Worker('./worker.js');
worker.postMessage({ type: 'start', payload: { count: 666 } }); // 发送信息给worker
terminate()

主线程通过此方法终止 Worker 的运行。

1
2
const worker = new Worker('./worker.js');
worker.terminate();

worker 作用域

Worker 的作用域跟主线程中的 Window 是相互独立的,并且 Worker 中是获取不到 DOM 元素的。所以在 Worker 中你无法使用 Window 变量。取而代之的是可以用 self 来表示全局对象。比较常用的方法是 onmessage、postMessage,主要用来跟主线程进行通信。

1
2
3
4
5
6
7
8
9
10
11
12
// 监听事件,主线程可以通过 postMessage 发送信息过来
self.onmessage = (messageEvent) => {
const { type, payload } = messageEvent.data;
switch (type) {
case 'start':
// 通过 type 去区分不同的业务逻辑,payload 是传过来的数据
const result = 0;
// ....,通过一系列处理之后,把最终的结果发送给主线程
this.postMessage(result);
break;
}
};

worker 引入其它脚本

1
2
3
// Worker.js
importScripts('constant.js');
// 下面就可以获取到 constant.js 中的所有变量了
1
2
3
4
5
6
7
8
9
10
11
12
13
// constant.js
// 可以在 Worker 中使用
const a = 111;

// 不可以在 Worker 中使用,原因未知
const b = function () {
console.log('test');
};

// 可以在 Worker 中使用
function c() {
console.log('test');
}

调试

![image-20221104134525616](Web Worker/image-20221104134525616.png)

控制台源代码中有专门的 worker.js

常用使用场景

视频网站

一般的视频网站 以优酷为例,当我们开始播放优酷视频的时候,就能看到它会调用 Worker,解码的代码应该写在 Worker 里面。

需要大量计算的网站

比如 imgcook 这个网站,它能在前端解析 sketch 文件,这部分解析的逻辑就写在 Worker 里。

一个例子说明 webworker 的效率

不使用 webworker,主线程运行3个斐波那契

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
function fb(n){
if(n==1||n==2){
return 1;
}
return fb(n-1)+fb(n-2)
}
console.time('三个fb执行时间');
var result=fb(40)
var result=fb(40)
var result=fb(40)
console.timeEnd('三个fb执行时间')
</script>
</body>
</html>

时间:9669ms

使用 webworker,创建三个进程,每个进程运行一个函数

1
2
3
4
5
6
7
8
9
10
11
12
13
// 主线程
var worker1=new Worker('worker1.js');
var worker2=new Worker('worker2.js');
var worker3=new Worker('worker3.js');
worker1.onmessage=e=>{
console.log(e.data);
}
worker2.onmessage=e=>{
console.log(e.data);
}
worker3.onmessage=e=>{
console.log(e.data);
}
1
2
3
4
5
6
7
8
9
10
11
12
// worker1.js
function fb(n){

if(n==1||n==2){
return 1;
}
return fb(n-1)+fb(n-2)
}
console.time('fb执行时间1');
var result=fb(40)
console.timeEnd('fb执行时间1')
self.postMessage('worker1');
1
2
3
4
5
6
7
8
9
10
11
12
// worker2.js
function fb(n){

if(n==1||n==2){
return 1;
}
return fb(n-1)+fb(n-2)
}
console.time('fb执行时间2');
var result=fb(40)
console.timeEnd('fb执行时间2')
self.postMessage('worker2');
1
2
3
4
5
6
7
8
9
10
11
12
// worker3.js
function fb(n){

if(n==1||n==2){
return 1;
}
return fb(n-1)+fb(n-2)
}
console.time('fb执行时间3');
var result=fb(40)
console.timeEnd('fb执行时间3')
self.postMessage('worker3');

时间分别为 2154、2185、2217

SharedWorker

SharedWorker 是一种共享数据的 Worker。它可以同时被多个浏览器环境访问。这些浏览器环境可以是多个 window, iframes 或者甚至是多个 Worker,只要这些 Workers 处于同一主域。为跨浏览器 tab 共享数据提供了一种解决方案。

创建 worker

1
const worker = new SharedWorker("./shareWorker.js"); // 参数是url,这个url必须与创建者同源 

SharedWorker 的一些方法

方法都在 port 上

port.onmessage
1
2
3
4
const sharedWorker = new SharedWorker('./shareWorker.js');
sharedWorker.port.onmessage = function (messageEvent) {
console.log(messageEvent)
}
port.postMessage()

参数格式不限

1
2
const sharedWorker = new SharedWorker('./shareWorker.js');
sharedWorker.port.postMessage({ type: 'increase', payload: { count: 666 } });
post.start()

主线程通过此方法开启 SharedWorker 之间的通信

1
2
const sharedWorker = new SharedWorker('./shareWorker.js');
sharedWorker.port.start()
port.close()

主线程通过此方法关闭 SharedWorker

1
2
const sharedWorker = new SharedWorker('./shareWorker.js');
sharedWorker.port.close()

例子

1
2
3
4
5
6
7
8
9
10
11
12
// index.js
const worker = new SharedWorker('./shareWorker.js');

worker.port.start(); // 开启端口

// 发送信息给shareWorker
worker.port.postMessage({ type: 'increase', payload: { count: 666 } });

// 接受shareWorker发过来的数据
worker.port.onmessage = function (val) {
console.log(val.data)
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// shareWorker.js
let count = 666;

port.onmessage = (messageEvent) => {
const { type, payload } = messageEvent.data;

switch (type) {
case 'increase':
port.postMessage(++count);
break;
case 'decrease':
port.postMessage(--count);
break;
}
};

调试

在浏览器中查看和调试 SharedWorker 的代码,需要输入 chrome://inspect/

![image-20221104140224970](Web Worker/image-20221104140224970.png)

Service Worker

ServiceWorker 一般作为 Web 应用程序、浏览器和网络之间的代理服务。他们旨在创建有效的离线体验,拦截网络请求,以及根据网络是否可用采取合适的行动,更新驻留在服务器上的资源。他们还将允许访问推送通知和后台同步 API。

创建 ServiceWorker

navigator.serviceWorker.register().then()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// index.js
if ('serviceWorker' in navigator) {
window.addEventListener('load', function () {
navigator.serviceWorker
.register('./serviceWorker.js', { scope: '/page/' })
.then(
function (registration) {
console.log(
'ServiceWorker registration successful with scope: ',
registration.scope
);
},
function (err) {
console.log('ServiceWorker registration failed: ', err);
}
);
});
}

只要创建了 ServiceWorker,不管这个创建 ServiceWorker 的 html 是否打开,这个 ServiceWorker 是一直存在的。它会代理范围是根据 scope 决定的,如果没有这个参数,则其代理范围是创建目录同级别以及子目录下所有页面的网络请求。代理的范围可以通过registration.scope 查看。

ServiceWorker 的一些方法

oninstall
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// serviceWorker.js
const CACHE_NAME = 'cache-v1';
// 需要缓存的文件
const urlsToCache = [
'/style/main.css',
'/constant.js',
'/serviceWorker.html',
'/page/index.html',
'/serviceWorker.js',
'/image/131.png',
];
self.oninstall = (event) => {
event.waitUntil(
caches
.open(CACHE_NAME) // 这返回的是promise
.then(function (cache) {
return cache.addAll(urlsToCache); // 这返回的是promise
})
);
};

在上述代码中,我们可以看到,在 install 事件的回调中,我们打开了名字为 cache-v1 的缓存,它返回的是一个 promise。在打开缓存之后,我们需要把要缓存的文件 add 进去,基本上所有类型的资源都可以进行缓存,例子中缓存了 css、js、html、png。如果所有缓存数据都成功,就表示 ServiceWorker 安装成功;如果控制台提示 Uncaught (in promise) TypeError: Failed to execute ‘Cache’ on ‘addAll’: Request failed,则表示安装失败。

onfetch
1
2
3
4
5
6
7
8
9
10
11
12
13
self.onfetch = (event) => {
event.respondWith(
caches
.match(event.request) // 此方法从 service worker 所创建的任何缓存中查找缓存的结果
.then(function (response) {
// response为匹配到的缓存资源,如果没有匹配到则返回undefined,需要fetch资源
if (response) {
return response;
}
return fetch(event.request);
})
);
};

在 fetch 事件的回调中,我们去匹配 cache 中的资源。如果匹配到,则使用缓存资源;没有匹配到则用 fetch 请求。正因为 ServiceWorker 可以代理网络请求,所以为了安全起见,规范中规定它只能在 https 和 localhost 下才能开启。

调试

在浏览器中查看和调试 ServiceWorker 的代码,需要输入 chrome://inspect/#service-workers

举个例子

两张图片,缓存了图片1,断网后,图片1仍能显示,控制台/网络能看到请求成功,200状态码后面写了(from service worker),实际上文件被存到了 cacheStorage 中,控制台/应用中可以看到。

使用场景

缓存资源文件,加快渲染速度

总结

类型 Worker SharedWorker ServiceWorker
通信方式 postMessage port.postMessage 单向通信,通过 addEventListener 监听 serviceWorker 的状态
使用场景 需要大量计算的场景 跨 tab、iframes 间共享数据 缓存资源、网络优化
兼容性 >= IE10 >= Chrome4 不支持 IE、Safari、Android、iOS
>= Chrome 4
不支持 IE
>= Chrome 40

Web Worker
http://example.com/2022/11/04/Web Worker/
Author
John Doe
Posted on
November 4, 2022
Licensed under