前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >异步JS中的Web Workers

异步JS中的Web Workers

原创
作者头像
BLUSE
修改2022-11-17 09:27:14
1.5K0
修改2022-11-17 09:27:14
举报
文章被收录于专栏:前端web技术前端web技术

一、了解Web Workers

介绍 js 的 Workers 前, 先思考什么是异步javascript? 为什么需要异步javascript的存在?

我们知道在编程模型上分为同步编程和异步编程:

1、同步编程和异步编程

同步编程即各任务按顺序一个一个执行, 前一个任务完全执行完后再执行下一个任务, 程序执行顺序跟编写的顺序是一致的, 逻辑比较清晰, 但遇到有耗时任务时容易产生阻塞.

异步编程即各任务不一定是按顺序执行的, 对于耗时的任务可以处理成异步任务, 异步任务开启后, 不等待执行结果就可以执行下一个任务, 对其他事件做出响应. 异步任务执行完后通过回调函数的方式将结果返回. 异步模式有很多, 例如setTimeout、ajax、fetch、getUserMedia、Promise、async/await等.

因为javascript是单线程的(注意浏览器不是单线程的, js调用其内部的api也不一定是单线程的, 如定时器), 其只有一个线程用来执行代码, 所以为了避免遇到计算量大、耗时的任务阻塞线程继续往下执行, js引入了事件循环的异步编程机制, 解决同步单线程的阻塞问题.

虽然有事件循环机制, 但其本质上还是在一个单线程上执行, 它在同一时间也只能做一件事情, 如果它正在等待长期运行的同步调用返回,就不能做其他任何事情. 有没有一种方法, 可以在多线程中并行执行某些任务? Workers 就赋予了在不同线程中运行某些任务的能力,因此你可以启动任务,然后继续其他的处理.

当然对于js的多线程的代码来说, 主线程代码和 Worker 线程代码是运行在完全分离的环境中,他们不能直接访问彼此的变量, 只能通过相互发送消息来进行交互. 因此 Workers 是不能访问 DOM(窗口、文档、页面元素等等)的.

2、Web Wokers

通过使用 Web Workers,Web 应用程序可以在独立于主线程的后台线程中,运行一个脚本操作。这样做的好处是可以在独立线程中执行费时的处理任务,从而允许主线程(通常是 UI 线程)不会因此被阻塞/放慢[MDN解释].

js中的Web Workers有三种类型:

  • Dedicated Workers: 专用线程, 由主线程实例化, 并且只能与之进行通信.
  • Shared Workers: 共享线程, 可以被运行在同源的所有进程访问(不同的浏览的选项卡,内联框架及其它shared workers), 可以由运行在不同窗口中的多个不同脚本共享.
  • Service Workers: 服务线程, 一个注册在指定源和路径下的事件驱动worker, 采用 js 控制关联的页面或者网站,拦截并修改访问和资源请求,细粒度地缓存资源. 可以在某些特定的情景下控制应用的行为, 如弱网环境下.

二、Dedicated Workers

通常所说的 Worker 是指Deicated Workers, 其接口是 Web Workers API 的一部分, 他可以由脚本创建后台任务, 在任务执行的过程中, 可以向其他创建者收发信息, 我们可以直接使用Web Workers API 的 Worker 构造函数创建实例, 所有Worker必须与其创建者同源.

1、示例

下面示例包含Worker的基本API, postMessage、onmessage、onmessageerror、terminate, 配合index.html, 直接在浏览器中启动即可看到效果

代码语言:javascript
复制
// main.js主线程
const first = document.querySelector('#number1');
const result = document.querySelector('.result');

if (window.Worker) {
  // 主线程创建worker线程
  const worker = new Worker("./worker/worker.js");
  
  // 发送一条消息到最近的外层对象,消息可由任何 JavaScript 对象组成
  first.onchange = () => { worker.postMessage(first.value); }

  // 当MessageEvent类型的事件冒泡到 worker 时,事件监听函数         EventListener 被调用
  worker.onmessage = (e) => { result.textContent = e.data; }

  // 当messageerror类型的事件发生时,对应的事件处理器代码被调用
  worker.onmessageerror= (e) => {}
  
  // 立即终止 worker。该方法不会给 worker 留下任何完成操作的机会;就是简单的立即停止
  result.onclick = () => { worker.terminate(); }
} else {
  console.log('Your browser doesn\'t support web workers.');
}
代码语言:javascript
复制
// worker.js, Worker线程接收主线程信息
onmessage = (e) => { postMessage('Worker Start'); };

整体的使用方式比较简单, 直接 new Worker 创建新的 Worker 线程, 执行 worker 的代码, 如果 worker 中执行计算密集型的耗时代码, 则不影响主线程的执行.

2、全局上下文

之前说到js中的主线程和 worker 线程是隔离的, 他们的变量是不能共用了, 只能通过 postMessage 进行消息传递, 其本质是 Worker 运行在另一个全局上下文中, 有自己的作用域, 与当前的 window 不同, 也无法直接访问Window对象. 但是 Web Workers API 提供了接口 WorkerGlobalScope 来访问一些Web API, 每个 WorkerGlobalScope 也都有自己的事件循环. 对于 Dedicated Workers 来说, 在 Worker 线程内提供了 DedicatedWorkerGlobalScope 对象, 他继承了 WorkerGlobalScope 属性, 可以通过Self来访问, 例如 self.location 会输出:

三、Shared Workers

其也是 Web Workers API 的一种共享线程, 说他共享是因为他可以从几个浏览上下文中访问, 例如几个窗口、iframe 或其他 worker. 对于同一个 worker url 只会创建一个 SharedWorker, 其他页面再使用同样的 url 创建 SharedWorker,会复用已创建的 worker,这个worker由那几个浏览上下文共享. 也因为他是可以跨窗口访问, 因此 SharedWorker 是通过活动端口 port 来发送和接收消息.

1、示例

单页面的Worker线程和主线程之间的通信与 Dedicated Workers 类似, 只不过是调用 SharedWorker 对象进行实例化, 这里不做举例. 下面主要对如何使用 SharedWorker 是进行多页面通信示例, 这里创建两个html页面:

代码语言:javascript
复制
// index.js, 做加法运算
const add = document.querySelector('#number1');
const result1 = document.querySelector('.result1');

if (!!window.SharedWorker) {
  // 初始化一个名为 myWorker 的 SharedWorker 实例
  const worker = new SharedWorker("./worker.js", 'myWorker');
  
  // worker.port是一个 MessagePort 对象用来进行通信和对共享 worker 进行控制
  add.onchange = () => { worker.port.postMessage([add.value, (add.value * 2) / add.value]);  }

  // 监听当前 port 的消息接收事件
  worker.port.onmessage = (e) => { result1.textContent = e.data; }
}
代码语言:javascript
复制
// index2.js, 做乘法运算
const square = document.querySelector('#number3');
const result2 = document.querySelector('.result2');

if (!!window.SharedWorker) {
  const worker = new SharedWorker("./worker.js", 'myWorker');

  square.onchange = () => { worker.port.postMessage([square.value, square.value]);  }

  worker.port.onmessage = (e) => { result2.textContent = e.data; }
}
代码语言:javascript
复制
// 共享worker.js
// 存放所有的port
const portPool = [];

onconnect = (e) => {
  const port = e.ports[0];

  // 在connect时将 port添加到 portPool中
	portPool.push(port);

  port.onmessage = (e) => {
    const workerResult = "Result: " + e.data[0] * e.data[1];
    // 一个port收到消息后, 就广播给所有的port
    portPool.forEach(port => {
      port.postMessage(workerResult);
    })
  };
};

2、调试

分别运行两个页面后(保证同源), 会看到worker.js只加载了一次, 下面分别是 index.html 和 index2.html 的 network 情况, 说明两个同源的页面是共享了同一个线程, 并且启动后, 刷新页面也不会重新去初始化worker, 除非关闭所有页面.

如果你使用的是chrome, 在地址栏输入chrome://inspect/#workers即可打开后台工具, 可以看到当前的一些workers, worker的名称是调用 new SharedWorker 时传入的 name, inspect 可以调起worker的调试工具窗口, terminate 可以手动停止线程

注意: 当你修改 worker 代码之后 Shared Workers 仍然会缓存之前运行的 worker 代码, 需要手动终止线程, 再重新启动

我们在index.html页面触发加法运算, 并 postMessage 给worker线程, 分别在不同的调试窗口可以看见对应的打印信息,

index.html: 主动触发 postMessage 后, 接收到了 worker 的计算结果

worker.js: worker 接收到来自 index.html 的post信息, 并进行计算, 将结果广播出去

index2.html: 接收到了 worker 的广播计算结果

3、全局上下文

与 Dedicated Workers 一样, Worker 线程内提供了 SharedWorkerGlobalScope 对象, 他继承了 WorkerGlobalScope 属性, 同样可以通过 self 来访问, 如 self.performance 会输出:

四、Service Workers

简称SW, 一般作为 web 应用程序、浏览器和网络(如果可用)之间的代理服务. 他们旨在(除开其他方面)创建有效的离线体验, 拦截网络请求, 以及根据网络是否可用采取合适的行动, 更新驻留在服务器上的资源. 他们还将允许访问推送通知和后台同步 API.[MDN解释]

简单理解, 其实就是有一个独立于当前网页线程的后台线程, 在网页发起请求时进行代理,并缓存相关文件, 以便用户可以进行离线访问. SW 也是 PWA(渐进式网页应用) 的重要组层部分, 许多技术框架(如React、Vue)会默认带上该功能.

1、使用前提

由于 SW 会作为代理服务出现, 并且会去拦截网络请求, 为避免中间人攻击和考虑到其他安全因素, 必须使用HTTPS来进行页面访问, 如果是本地开发, localhost也被认为是安全的.

有些支持 SW 的浏览器版本可能默认开启一些特性, 导致无法正常运行 SW, 可以进行响应的配置, 例如

  • Firefox Nightly: 访问 about:config 并设置 dom.serviceWorkers.enabled 的值为 true; 重启浏览器
  • Chrome Canary: 访问 chrome://flags 并开启 experimental-web-platform-features; 重启浏览器 (注意:有些特性在 Chrome 中没有默认开放支持)
  • Opera: 访问 opera://flags 并开启 ServiceWorker 的支持; 重启浏览器

2、使用方式

SW的调用可以拆分为以下几个阶段, 也即生命周期

1) 注册和下载

在主线程中进行 SW 注册, 首次注册使用 navigator.serviceWorker.register 方法, 创建一个给定 scriptURL 的 ServiceWorkerRegistration, 调用时会立刻去下载对应的 scriptURL 文件, 代码如下, 其中scope 表示 SW 可以控制的 URL 范围. 通常是基于当前的 location来解析传入的路径.

代码语言:javascript
复制
const registerServiceWorker = async () => {
  if ('serviceWorker' in navigator) {
    try {
      const registration = await navigator.serviceWorker.register(
        '/sw/sw.js',
        { scope: '/sw/' 
}
      );
    } catch (error) {
      console.error(`Registration failed with ${error}`);
    }
  }
};

注册成功之后 SW 会在在 ServiceWorkerGlobalScope 环境中运行, 与另外两种 Workers 类似, 这也是一个独立于主线程的 worker 全局上下文.

2) 安装

注册和下载之后浏览器会进入安装阶段, 可以通过 install 事件进行监听, 并且在这个事件里可以对站点资源进行缓存

代码语言:javascript
复制
self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open('v1').then((cache) => cache.addAll([
      '/sw/',
      '/sw/index.html',
      ...
    ]))
  );
});
  1. event.waitUntil 方法告诉浏览器该事件仍在进行, 直到内部的 promise 解决,浏览器都不应该在事件中的异步操作完成之前终止 SW 线程. 如果 promise reject, 则此次安装被认为失败, 会丢弃这个 SW 线程.
  2. caches 是一个 CacheStorage 对象, caches.open(‘v1’) 会打开一个名为 v1 都 Cache 对象, 再使用 Cache 对象的方法去处理缓存, 例如 addAll 会抓取一个 URL 数组,检索并把返回的 response 对象添加到给定的 Cache 对象中
  3. 激活

安装完成后, 会接收到一个激活事件, 在该事件中可以进行一些缓存的清理工作

代码语言:javascript
复制
const enableNavigationPreload = async () => {
  const cacheWhitelist = ['v1'];
  const keyList = await caches.keys();
  await  Promise.all(keyList.map((key) => {
    if (!cacheWhitelist.includes(key)) {
      return caches.delete(key);
    }
  }));
};

self.addEventListener('activate', (event) => {
  event.waitUntil(enableNavigationPreload());
});

如果有新版本的 SW , 浏览器会去下载, 随着业务不断更新, 缓存中会出现多个版本的 SW 资源, 这个时候需要定期地清理缓存条目, 因为每个浏览器都硬性限制了一个域下缓存数据的大小, 浏览器会尽其所能去管理磁盘空间,但它有可能删除一个域下的缓存数据。浏览器要么自动删除特定域的全部缓存,要么全部保留. 因此为了更好的管理, 我们可以手动调用 caches.delete 方法删掉对应 key 值的Cache 条目.

3) 更新

当重新进入 SW 页面, 或者在 SW 上的一个事件被触发并且过去 24 小时没有被下载时会触发更新, 如果下载的 SW 文件是新的, 安装就会在后台尝试进行, 安装成功后不会被激活, 会进入 waiting 阶段, 直到所有已加载的页面不再使用旧的 SW 才会被激活.

4) fetch

还有一个值得监听的重要事件是 fetch, 他是进行自定义请求响应的, 每次请求被 SW 控制的资源时,都会触发 fetch 事件,这些资源包括了指定的 scope 内的文档, 和这些文档内引用的其他任何资源. 可以在该监听事件中做一些操作, 比如将请求资源写入缓存、控制资源获取优先级等. event.respondWith 正好能为我们劫持 HTTP 请求来执行自己方法.

代码语言:javascript
复制
const putInCache = async (request, response) => {
  const cache = await caches.open('v1');
  await cache.put(request, response);
};

const cacheFirst = async ({ request, preloadResponsePromise, fallbackUrl }) => {
  // 尝试等待预加载响应, 以便追踪请求发出情况
  const preloadResponse = await preloadResponsePromise;
  if (preloadResponse) {
    console.info('using preload response', preloadResponse);
    putInCache(request, preloadResponse.clone());
    return preloadResponse;
  }

  // 尝试直接发起网络请求
  try {
    const responseFromNetwork = await fetch(request);
    // 缓存响应
    putInCache(request, responseFromNetwork.clone());
    return responseFromNetwork;
  } catch (error) {
    // 请求失败时, 尝试获取缓存资源
    const responseFromCache = await caches.match(request);
    if (responseFromCache) {
      return responseFromCache;
    }

    // 请求失败时, 查找降级资源, 如果找到则返回
    const fallbackResponse = await caches.match(fallbackUrl);
    if (fallbackResponse) {
      return fallbackResponse;
    }
    // 如果没有, 则直接返回失败
    return new Response('Network error happened', {
      status: 408,
      headers: { 'Content-Type': 'text/plain' },
    });
  }
};

self.addEventListener('fetch', (event) => {
  event.respondWith(
    cacheFirst({
      request: event.request,
      preloadResponsePromise: event.preloadResponse,
      fallbackUrl: '/sw/gallery/myLittleVader.jpeg',
    })
  );
});

3、效果展示

当访问页面时, 会去拉取对应的资源, 通过Network、Application、chrome://inspect/#service-workers, 可以查看相应的状态

首先查看NetWork, 可以看到部分资源已经从 SW 的缓存中获取, 此时将网络断开, 发现缓存的资源仍然可以获取到, 页面仍然可以正常访问

再看看Application的Cache Storage, 可以看到以 key 值 v1 存储的响应缓存, 这些缓存文件都是我们在 install 中添加到我们待缓存的列表中的文件路径

在 Application 的 Service Workers 中可以看到对应 SW的一些状态记录, 以及可以对其进行相应的操作

同样使用 chrome 开发者工具, 可以查看 SW 线程的一些相关信息, 以及终止 SW 线程

4、其他应用场景

SW 功能强大, 不仅可以用作网页的离线访问, 还有很多其他的用途, 也有很多三方库的封装, 例如 Workbox, SW 还可以运用于:

  • 后台数据同步
  • 消息推送集中接收计算成本高的数据更新,比如地理位置和陀螺仪信息,这样多个页面就可以利用同一组数据
  • 在客户端进行 CoffeeScript,LESS,CJS/AMD 等模块编译和依赖管理(用于开发目的)
  • 自定义模板用于特定 URL 模式
  • 性能增强,比如预取用户可能需要的资源,比如相册中的后面数张图片

……

五、总结

在 js 的单线程运行环境外加时间循环机制的加持下, 我们可以比较方便处理我们的一些同步和异步逻辑, 不过有时面对计算密集型、耗时高、性能要求高、网络环境差等场景下, 我们可以使用更为有效的 Web Workers 开辟多线程去进行一些优化. 其实除了 Web Workers 中的多线程, Nodejs中同样也有相应的多线程处理方式, 可见多线程的作用之大. 而 Web Workers 除了上面说的三种类型, 还包括音频 Workers、Chrome Workers 等等, 也都在特定的场景中非常有用.

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、了解Web Workers
    • 1、同步编程和异步编程
      • 2、Web Wokers
      • 二、Dedicated Workers
        • 1、示例
          • 2、全局上下文
          • 三、Shared Workers
            • 1、示例
              • 2、调试
                • 3、全局上下文
                • 四、Service Workers
                  • 1、使用前提
                    • 2、使用方式
                      • 1) 注册和下载
                      • 2) 安装
                      • 3) 更新
                      • 4) fetch
                    • 3、效果展示
                      • 4、其他应用场景
                      • 五、总结
                      相关产品与服务
                      云开发 CLI 工具
                      云开发 CLI 工具(Cloudbase CLI Devtools,CCLID)是云开发官方指定的 CLI 工具,可以帮助开发者快速构建 Serverless 应用。CLI 工具提供能力包括文件储存的管理、云函数的部署、模板项目的创建、HTTP Service、静态网站托管等,您可以专注于编码,无需在平台中切换各类配置。
                      领券
                      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档