likes
comments
collection
share

由面试题引发的Worker Study

作者站长头像
站长
· 阅读数 48
  • 由场景题扯到了这个
  • 我: WebWorker我了解到的有两种,一种是Service Worker, 另一种是Shared Worker
  • 面试官: 你再想想?WebWorker和Service Worker应该是两种东西, 不太一样, 你了解PWA吗
  • 我: 啊??-------
  • 面试官: 那你说说怎么使用Service Worker?
  • 我: emmm.....不会(我只了解过SharedWorker是new一个实例, 然后用postMessgae通信)
  • MDN在介绍Web Worker时有以下描述, 有许多不同类型的 worker:
    • 专用 worker
    • Shared worker
    • Service Worker
  • 故我理解的面试官应该指的不一样应该是专用Worker和Service Worker
  • 当然, 答案不是很重要, 掌握好具体是什么, 什么场景使用,如何使用才最重要, 耶

一、什么是Web Worker

1. 先看MDN介绍Web Worker 使得在一个独立于 Web 应用程序主执行线程的后台线程中运行脚本操作成为可能。这样做的好处是可以在独立线程中执行费时的处理任务,使主线程(通常是 UI 线程)的运行不会被阻塞/放慢

  • Web Worker 的运行不会影响主线程,不过在和主线程进行通信的时候依旧会受到主线程单线程的瓶颈制约

2. 限制

  • 不能直接在worker线程中操作 DOM 元素(不能操作DOM的原因我想也很简单, 本来JS设计为单线程就有避免出现多个线程同时操作DOM的情况)
  • 不能使用 window对象中的某些方法和属性(但是存在WorkerGlobalScope

3.worker可以依次生成新的 worker,只要这些 worker 与父页面托管在同一个origin

二、什么是专用Worker

  1. 定义: 单个脚本使用的 worker(我的理解就是真正用于处理一些费时间任务的Worker
  2. 上下文DedicatedWorkerGlobalScope
  3. 相关API
  • Worker: 构造函数, 可以通过new Worker('xxxxx/xxx.js')来创建工作线程
  • Worker.prototype.onmessage: 接受另一个线程的回调函数, 其实就是监听发来的postMessage
  • Worker.prototype.postMessgae: 给另一个线程发送数据
  • Worker.prototype.terminate: 终止线程
  1. 具体来个例子尝试一下
  • 尝试两种情况:

    • 只用单线程,计算三次斐波那契数列
    export const commonDo = () => {
        const startTime = Date.now();
        const res1 = timeConsumingCode(45);
        const res2 = timeConsumingCode(45);
        const res3 = timeConsumingCode(45);
        document.querySelector('#commonResult').innerText = res1 + res2 + res3;
        const endTime = Date.now();
        document.querySelector('#commonTime').innerText = endTime - startTime;
    }
    
    • 使用worker线程,辅助计算三次斐波那契数列
    export const workDo = () => {
        let curTime = 0;
        let curSum = 0;
        const startTime = Date.now()
        // 这里开了三个worker线程去分别计算
        const work1 = new Worker('./3.js');
        const work2 = new Worker('./3.js');
        const work3 = new Worker('./3.js');
        const onmessageHanlde = (res) => {
            curSum += Number(res.data);
            curTime++;
            console.log(res);
            if (curTime === 3) {
                document.querySelector('#workerTime').innerText = Date.now() - startTime;
                document.querySelector('#workerResult').innerText = curSum;
                // 计算完后, 这里要终止线程的运行
                work1.terminate();
                work2.terminate();
                work3.terminate();
            }
        }
        // 通过onmessage的方法监听
        work1.onmessage = onmessageHanlde
        work2.onmessage = onmessageHanlde
        work3.onmessage = onmessageHanlde
    }
    
    

    可以看到当我们开启线程之后,source会显示有三个在运行, 当我们terminate之后, 这三个又会消失

由面试题引发的Worker Study 由面试题引发的Worker Study 录制performance之后也能看到有对应的三个线程在执行 由面试题引发的Worker Study

  • 对应的耗时情况
    • 对于第一种情况, 单线程计算的情况下耗时29896
    • 对于第二种情况, 用worker辅助计算的情况下耗时10811 由面试题引发的Worker Study
    • 第二种总会略大于第一种的三分之一, 我理解在创建线程,以及线程通过postMessage通信都是存在耗时的
      • Web Worker之间不能共享内存, 主页面和Worker之间的数据传递是通过拷贝而不是共享来完成的
      • 在数据交换期间, 需要对数据序列化和反序列化

二、什么是Service Worker

  1. 先看介绍: Service worker 本质上充当 Web 应用程序、浏览器与网络(可用时)之间的代理服务器。这个 API 旨在创建有效的离线体验,它会拦截网络请求并根据网络是否可用来采取适当的动作、更新来自服务器的的资源。它还提供入口以推送通知和访问后台同步 API
  • Service Worker主要包括注册,安装,激活, 运行几个阶段
  • Service Worker 会常驻在浏览器, 即使注册他的页面已经关闭,主要手动关闭或者浏览器回收才会终止这个线程, 是服务于多个页面的
  • 在访问页面时会进入 activate 状态,在浏览器或者当前tab页关闭时会自动休眠,减少资源损耗
  1. 限制(仅针对于Service Worker)
  • 出于安全考量,Service worker 只能由 HTTPS 承载(这个我想也很容易理解, 因为Service Worker是可以拦截网络请求的, 一旦暴露给中间人攻击那么很危险, 而HTTPS可以有效解决中间人攻击)
  1. 实践出真知
  • 注册: Service Worker是通过navigator.serviceWorker.register 来进行注册的, 当然,使用的时候一般需要判断navigator是否存在serviceWorker这个属性(基于兼容性的考虑) 由面试题引发的Worker Study
  • 安装: 是在ServiceWorker注册成功之后,浏览器开始下载ServiceWorker脚本的阶段, 我们可以通过install事件去对它做一个监听
addEventListener('install', () => {
    console.log('install........')
})
  • 激活: 是在安装完ServiceWorker脚本后, 对他进行一个激活的行为, 我么可以通过activate事件去做一个监听
addEventListener('activate', () => {
    console.log('activate.....');
})
  • 运行: 是运行阶段是在激活完成之后,ServiceWorker开始运行的阶段, 我们可以通过fetch事件去做一个监听
addEventListener('fetch', () => {
    console.log('fecth......');
})

我们也可以通过registration的相关字段去做判断

const registerServiceWorker = async () => {
    if ("serviceWorker" in navigator) {
      try {
        const registration = await navigator.serviceWorker.register("./2.js");
        if (registration.installing) {
          console.log("正在安装 Service worker...........");
        } else if (registration.waiting) {
          console.log("已安装 Service worker installed..........");
        } else if (registration.active) {
          console.log("激活 Service worker.........");
        }
      } catch (error) {
        console.error(`注册失败:${error}`);
      }
    }
};

在理解实际运用场景之前, 我决定先从面试官提到的PWA入手

4. 什么是PWA

  • Progressive Web Apps(渐进式增强 WEB APP): 是一种结合了传统网页和原生应用功能的Web应用程序。它具有以下特点和优势
    • 渐进式增强
    • 应用程序外观
    • 响应式布局
    • 离线访问:PWA具备离线访问的能力,即使在网络连接不可用的情况下,用户仍然可以继续访问应用程序

诶注意到离线访问能力, 这不就可以通过使用Service Worker技术,去拦截请求缓存应用程序的资源,从而实现离线访问和更快的加载速度嘛

5. 离线缓存

要实现离线缓存, 我想应该有四个阶段, 第一个阶段为安装激活Service Worker(上面讲过了), 第二个阶段拦截请求对结果进行缓存, 第三阶段为请求时取出缓存直接使用缓存。

我们挨个突破, 首先要了解的是跟 缓存相关的API

  • 通过caches.open(CacheName)去打开一个缓存对象
  • 通过Cache.put(req, res)抓取一个请求及其响应,并将其添加到缓存中
  • 通过Cache.add(req)抓取这个 URL,检索并把返回的 response 对象添加到给定的 Cache 对象
  • 通过Cache.addArr(url[])抓取一个 URL 数组,检索并把返回的 response 对象添加到给定的 Cache 对象
  • 通过Cache.match(req, opts)返回一个 Promise对象,resolve 的结果是跟 Cache 对象匹配的第一个已经缓存的请求

这个 API 和浏览器的标准的缓存工作原理很相似,但它特定于你的域的。直到你清理它们之前,这些内容都会持久存在

先看第一阶段, 按照我们上述的去激活就OK了, 我们可以从application中看到对应的Service Worker的状态

由面试题引发的Worker Study

其次开始我们的第二阶段,以下代码我们通过install事件去监听了service Worker的注册, 接着调用了e.waitUntil确保我们能够在安装之前将对应的代码执行完成, 接着调用了caches.open()打开了对应的缓存, 然后使用addAll将我们所需要的缓存的url(相对于Worker)加入

const cacheName = 'mouche_cache_v1';
addEventListener('install', (e) => {
   e.waitUntil(
        caches.open(cacheName).then((cache) => {
            return cache.addAll([
                './1.html',
                './1.js',
                './3.js',
                './4.css',
            ])
        })
   )
})
  • 成功缓存后我们也可以在application中找到缓存空间, 可以看到相关的内容已经缓存成功了

由面试题引发的Worker Study

然后开启我们的第三阶段, 每次获取 service worker 控制的资源时,都会触发 fetch 事件,通过fetch拦截请求之后, 判断是否缓存中有对应的url, 有的话则直接取出结果, 调用 e 上的 respondWith() 方法来劫持我们的 HTTP 响应,没有的话则发起请求再做一次缓存(注意这里的不需要去等待缓存)

该匹配通过 URL 和各种标头进行

const putToCache = async (req, res) => {
    const cache = await caches.open(cacheName);
    await cache.put(req, res);
}
const findCache = async (req) => {
    const resFromCache = await caches.match(req);
    if (resFromCache) {
        return resFromCache;
    } else {
        const resFromNetWork = await fetch(req);
        // 这里必须克隆,因为请求和响应流仅可以读取一次
        // 为了返回响应到浏览器,并将其放入缓存中
        // 因此原始的资源会返回给浏览器,克隆的资源会发送到缓存。它们都只能被读取一次
        putToCache(req, resFromNetWork.clone());
        return resFromNetWork;
    }
}

addEventListener('fetch', (e) => {
    if (e.request) {
        e.respondWith(findCache(e.request));
    }
})

由面试题引发的Worker Study

  • 我们尝试离线, 可以看到,资源都能够正常地从Service Worker获取到 由面试题引发的Worker Study 由面试题引发的Worker Study

OK, 这样已经能够做到,缓存资源-取出缓存应用的过程了, 但是我们还没有考虑到更新缓存, 当我们被缓存的文件更改的时候, Service Worker是不感知的, 那么我们在文件更新(发布新版本)的情况下则需要修改CacheName, 让其存放到新的缓存中。 由面试题引发的Worker Study 那么此时我们还需要对旧缓存进行一个清理,否则资源会越累积越多 那么我们一般是在activate阶段去清理旧缓存

在activate进行更新的原因是:当我们使用旧版本缓存展示的页面时,新版本Service Worker的会安装但是不会激活, 会等到没有使用旧版本的的页面时, 新的才会被激活, 故在active适合去删除旧版本的缓存

addEventListener('activate', (e) => {
    e.waitUntil(
        caches.keys().then((cacheNames) => {
            return Promise.all(
                cacheNames.map((cName) => {
                    if (cName !== cacheName) {
                        return caches.delete(cName);
                    }
                })
            )
        })

})
  • 此时如图所示, 废弃的v1版本缓存已经被清除了 由面试题引发的Worker Study

三、什么是Shared Worker

  1. 先看MDN的介绍: SharedWorker 接口代表一种特定类型的 worker,可以从几个浏览上下文中访问,例如几个窗口、iframe 或其他 worker

看介绍和名字就能知道了他是用了进行多个页面中进行通信的手段, 回顾一下, 此类型的手段已经有

  • 通过BroadcastChannel开启一个频道用于同源的页面进行通信
  • 通过localStroage存储和监听storage
  • 通过轮询 + cookie存储
  • 通过iframe/window.open打开的页面可以直接使用postMessage通信
  • 通过websocket做中间商
  1. 限制:
  • 如果要使 SharedWorker 连接到多个不同的页面,这些页面必须是同源
  1. 使用:
  • 来个简单的demo, 背景是我使用多个tab打开了同一页面,此时接收到后端的推送, 会弹出弹窗, 此时两个页面都会弹出, 那我自然是希望,我点掉一个页面的弹窗时,另一个页面对应的弹窗也可以自动消失, 此时就可以通过Shared Worker去实现
  • 通过new SharedWorker()去开启一个Worker
  • 通过worker.port.postMessage向Shared Worker发送消息
  • 通过worker.port.onmessage 监听Worker发送过来的消息
const worker = new SharedWorker('./2.js');
btn.addEventListener('click', () => {
    const newDom = document.createElement('div');
    newDom.innerText = index;
    newDom.setAttribute('class', 'child');
    newDom.addEventListener('click', () => {
        worker.port.postMessage(newDom.innerText); // 向Worker线程发送消息
    })
    rootDiv.appendChild(newDom);
    index++;
})

worker.port.onmessage = (e) => {
    const receviceData = e.data;
    const targetDom = Array.from(rootDiv.children).filter((x) => x.innerText === receviceData);
    targetDom && rootDiv.removeChild(targetDom[0]);
}
  • 在Worker文件中, 维护一个port池,onconnect的触发时机为页面开启Worker, 此时将页面port加入我们的port池,给它开启message监听, 当有页面向Worker发消息, Worker向port池中的所有页面推送消息
const portPool = [];
onconnect = (e) => {
    const port = e.ports[0];
    portPool.push(port);
    port.addEventListener('message', (e) => {
        const workerResult = e.data;
        portPool.forEach((port) => port.postMessage(workerResult));
    })
    port.start();
}
  • 效果如下:无论点击哪个页面, 其他页面都会随之更新 由面试题引发的Worker Study

四、 兼容性

由面试题引发的Worker Study

由面试题引发的Worker Study

由面试题引发的Worker Study 答案不重要, 重要的是学会了这几种的学费了

转载自:https://juejin.cn/post/7283813865719644199
评论
请登录