面试准备-web worker

895 阅读19分钟

前言

大家都知道在浏览器环境下JS是单线程执行的,估摸着应该是为了执行方面性能的考虑,增加了web worker api,可以让浏览器在后台开辟一个worker线程,处理某些事务。那么基于我们之前的《面试准备-异步》的学习中我们已经知道,web worker api是有一个专门的agent(代理),并且我们也通过不同agent知道在web中至少存在3种类型的worker分别是:

  • Web Worker
  • Shared worker
  • Service Worker

我们今天就来一起学习一下,web worker api到底是怎么回事

worker是什么?

worker是允许你在浏览器环境在后台开辟一个线程去执行脚本的api。当然它自身也有一定的限制,比如它无法直接访问DOM元素,或者无法使用document,window。因为它的执行上下文与主线程的执行上下文存在一定差异。不同的Worker有不同的全局对象。但是Worker是可以操作ajax或者IndexedDB等。所以我们可以理解为,worker可以帮我们处理一些幕后的事情,比如进行大量的数据计算,发起高频的网络请求或者是使用数据存储相关的api操作等。同时在whatwg可以了解到,worker的实际上是一个较重的API它对浏览器是有一定的负担的,所以建议上也是需要认真分析当前的场景是否是需要使用worker进行业务的实现或者性能的优化。 我们来看看兼容情况

image.png

这里是关于Worker API的兼容性情况,还是很不错的但是针对不同的Worker类型,兼容情况可能都有不同,我们在使用对应的Worker时,也是需要去检查一下对应的兼容性情况,再确定是否使用。

那么简单的介绍就结束了,接下来就让我们依次了解一下不同Worker的作用以及能力吧

web worker

web 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;

构造函数

我们可以看到worker继承于EventTarget,它本身就是采用的浏览器事件接口的。除此之外可以了解到,构造函数接受两个参数分别是scriptURL以及options

  • scriptURL 用于指定要加载的worker模块的脚本地址,部分浏览器环境支持使用data URI

  • options 对象类型,然后有三个可选的属性值,分别是type,credentials以及name

  • options.type 选择加载类型,可以使用的值有classic以及module,使用module允许使用ES Module对文件进行处理

  • options.credentials 用于制定加载时文件时,是否携带cookie等身份认证信息,点击查看详细介绍

  • options.name worker的名称,据MDN上说一般用于调试

那么了解完构造函数的的参数后,我们来尝编写一下自己的worker脚本,首先我们需要一个静态资源服务服务,搭建的方式可以参考《面试准备-JS模块化》中的服务搭建内容来进行,搭建完成后我们来尝试一下,首先我们编写*html如下

<!DOCTYPE html>
<html>
    <head>
      <meta charset="utf-8">
      <title>web worker api</title>
      <style>
      </style>
    </head>
    <body>
      <script>
        const myWorker = new Worker('./myWorker.js')
        myWorker.postMessage({name: 'mainMessage'})
        myWorker.addEventListener('message', (...argements) => {
          console.log(argements)
        })

      </script>
    </body>
</html>

然后编写我们的worker文件myWorker.js其中内容如下:

console.log(self)
function run () {
  setTimeout(() => {
    self.postMessage('workMessage')
  }, 1000)
}
self.addEventListener('message', (...argements) => {
  console.log(argements)
  run()
})

浏览器输入完地址以后我们打开控制台可以看到以下内容

image.png

其中第一条是我们在myWorker的全局执行上下文DedicatedWorkerGlobalScope,我们来一起了解一下。

首先我们需要知道一下DedicatedWorkerGlobalScope的继承关系。

image.png

首先DedicatedWorkerGlobalScope继承于EventTargetWorkerGlobalScope。可以使用父类的方法。那么同时我们也可以分析出来,实际上worker也就是一个事件目标对象,它也可以挂载EventListener进行时间监听。那么在了解完继承关系后我们再来梳理一下WorkerGlobalScope提供了什么方法以及属性(EventTarget之前在事件的文章中已经介绍过了,这里就不重复介绍了)

WorkerGlobalScope

WorkerGlobalScope是继承了EventTarget,并且实现了接口WindowTimersWindowBase64WindowEventHandlers, GlobalFetch

它拥有以下属性以及方法:

返回与当前上下文相关的CacheStorage对象,它主要与缓存相关,一般用于service worker中,所以在后文我们会详细介绍它

自带属性以及方法

返回与worker关联的 WorkerNavigator。它是一个特定的导航器对象,适用worker

返回对 WorkerGlobalScope 本身的引用。大多数情况下,它是一个特定的范围,例如 DedicatedWorkerGlobalScope、SharedWorkerGlobalScope (en-US) 或 ServiceWorkerGlobalScope。

返回与worker关联的 WorkerLocation。它是一个特定的位置对象,适用于worker

从其他接口实现的

(大部分就不解释了,大家都懂的)

数据方法对象

大部分的古有对象构造方法以及数据构造方法都是有的

浏览器存储

这里需要注意的是sessionStoragelocalStorage是没有办法在WorkerGlobalScope中使用的。在worker中可以使用的浏览器存储有IndexedDB。后续会专门出一期专门介绍浏览器的存储的文章

DedicatedWorkerGlobalScope

事件

DedicatedWorkerGlobalScope实际上在WorkerGlobalScope的基础上增加了两个事件分别是:

  • message

来源构建实例postMessage的信息

  • messageerror 收到无法反序列化的message时的报错

支持以DOM0的传统模式以及DOM2的EventListener的方式进行监听

方法

以及增加了事件派发的方法:

总结

那么我们在了解完了以后实际上发现,web worker实际的应用场景是会需要跟我们同源窗口代理(具体可看异步章节学习)进行隔离的,针对视图导航的方面的能力从DedicatedWorkerGlobalScope的上下文出发我们可以发现基本是没有相关操作能力的。更多的主要是网络请求,数据处理(比如可转移对象),数据存储(IndexedDB)。比如数据大量计算,以及存储任务,处理任务等跟实时用户交互无关的任务可以交给它。

worker实例对象

了解完worker内部的全局对象后,我们再来了解一下worker实例。Worker在上文提到过,也是继承于EventTarget所以具备时间目标的相关属性以及方法。除此以外它还具备以下方法以及事件

方法

  • Worker.postMessage() 可以用于跟worker内部的上下文进行通讯,同DedicatedWorkerGlobalScope.postMessage方法参数

  • Worker.terminate() 结束当前worker的行为,不会等待worker完成剩余的操作(作者认为这里等同于worker上下文内部调用close方法,结束当起当前agents的事件循环,并进行回收)

事件

  • message 用于接收来自于worker内部上下文的message。

  • messageerrorDedicatedWorkerGlobalScopemessageerror事件,当worker内给外部实例传递一条无法返序列化的数据是有此报错

  • error 当在worker内执行上下文抛出错误时,会触发当前事件

Shared worker

SharedWorker允许多个同源窗口或者iframe对其进行同时访问,它跟web worker具有不同的全局环境SharedWorkerGlobalScope,那么首先我们先来尝试进行编写一个SharedWorker的例子。

<!--index.html-->
<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8">
  <title>web worker api</title>
  <style>
  </style>
</head>

<body>
  <script>
    const shareWorker = new SharedWorker("./myWorker.js");
    // 接受信息
    shareWorker.port.onmessage = (e)=> { 
      console.log(e.data)
    }
    // 发送信息
    shareWorker.port.postMessage({
      name: 'i am index.html'
    });
  </script>
</body>

</html>
<!--home.html-->
<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8">
  <title>web worker api</title>
  <style>
  </style>
</head>

<body>
  <script>
    const shareWorker = new SharedWorker("./myWorker.js");
    // 接受信息
    shareWorker.port.onmessage = (e)=> { 
      console.log(e.data)
    }
    // 发送信息
    shareWorker.port.postMessage({
      name: 'i am home.html'
    });
  </script>
</body>

</html>
// myWorker.js
const allContenter = []
self.onconnect = function (e) {
  const port = e.ports[0]
  allContenter.push(port)
  port.addEventListener('message', function (e) {
    allContenter.forEach(item => {
      item.postMessage(e.data)
    })
  })
  port.start()
}

然后我们在按照顺序分别打开index.html以及home.html然后我们来查看index.html的控制台打印

image.png

我们可以看到在index.html打印出了我们在home.html 中所派发的message,OK到这里案例结束了。那么我们一起来学习一下Shared worker的相关知识吧!

构造函数

首先Shared worker的构造函数与Worker是类似的都可以传递两个参数,区别点在于第二个参数在SharedWorker中可以选择传递name或者是option。其中name实际上等同于option.name

实例对象

我们发现我们在使用SharedWorker实例时主要是port属性进行调用的,port返回了一个MessagePort 对象,用于通信和控制SharedWorker

SharedWorker操作主要与MessagePort相关,那么我们来看看什么是MessagePort

MessagePort

首先MessagePort对象属于MessageChannel的两个通信端口之一。那么我们可以猜测每次调用SharedWorker实际上内部实现上调用了一次MessageChannel构造函数,并分别派发到了worker上下文以及当前浏览上下文的worker实例中。

接着由于MessagePort实际上是继承于EventTarget,所以存在并实现父类的方法

方法

事件

当 MessagePort 对象收到消息时会触发。 也可以通过 onmessage 属性使用。

当 MessagePort 对象收到无法被反序列化的消息时触发。 也可以通过 onmessageerror 属性使用。

那么了解完了MessagePort,我们再来了解一下SharedWorker的全局对象SharedWorkerGlobalScope

SharedWorkerGlobalScope

SharedWorkerGlobalScope的继承关系如下图:

image.png

也就是说基于我们上文的学习,我们只需要了解SharedWorkerGlobalScope本身所实现的方法/事件即可

方法

  • SharedWorkerGlobalScope.close() 同web worker的close方法 丢弃事件循环中的所有任务,然后关闭。

事件

当新的客户端(浏览上下文)连接到SharedWorker时触发

总结

那么到这里我们就完成了对SharedWorker的学习了解,后续我们有存在跨窗口或者iframe传递数据的需求时,我们就可以根据兼容性,选择使用当前方案完成场景

Service Worker

Service Worker我们先来根据MDN上的简介对其了解一下,它跟我们之前接触的worker可能存在一定的差异。所以大家在学习的时候可能要先暂时放一放之前对于worker的理解来看待Service Worker

简介

Service workers 本质上充当 Web 应用程序、浏览器与网络(可用时)之间的代理服务器。这个 API 旨在创建有效的离线体验,它会拦截网络请求并根据网络是否可用来采取适当的动作、更新来自服务器的的资源。它还提供入口以推送通知和访问后台同步 API。

Service worker是一个注册在指定源和路径下的事件驱动worker。它采用JavaScript控制关联的页面或者网站,拦截并修改访问和资源请求,细粒度地缓存资源。你可以完全控制应用在特定情形(最常见的情形是网络不可用)下的表现。

Service worker运行在worker上下文,因此它不能访问DOM。相对于驱动应用的主JavaScript线程,它运行在其他线程中,所以不会造成阻塞。它设计为完全异步,同步API(如XHRlocalStorage (en-US))不能在Service worker中使用。

出于安全考量,Service workers只能由HTTPS承载,毕竟修改网络请求的能力暴露给中间人攻击会非常危险。在Firefox浏览器的用户隐私模式,Service Worker不可用。

根据阅读上文可知Service workers常用于离线体验的提供,实际上像是Vue的官网已经在使用这一项技术,大家可以尝试访问后开启飞行模式,重新刷新页面,依旧可以进行访问

同时除去提供一些基本的离线支持外,可以用来尝试以下等场景

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

未来service workers能够用来做更多使web平台接近原生应用的事。 值得关注的是,其他标准也能并且将会使用service worker,例如:

  • 后台同步:启动一个service worker即使没有用户访问特定站点,也可以更新缓存
  • 响应推送:启动一个service worker向用户发送一条信息通知新的内容可用
  • 对时间或日期作出响应
  • 进入地理围栏

生命周期

单个Service Worker是存在声明周期的,整体而言包含以下生命周期:

  • 下载
  • 安装(指定某些安装工作,比如离线缓存)
  • 激活(当没有窗口在使用当前版本的worker时在后台激活)

以上为MDN介绍时的生命周期,实际上我在Chrome DevTool中可以发现还有一个Wait的阶段

所以实际上每个Service Worker的生命周期应该如下

  • 下载
  • 安装Install(指定某些安装工作,比如离线缓存)
  • 等待激活 Wait(因为当前还有在工作的service worker 所以要等待本次会话窗口全部结束后才会激活)
  • 激活Activate(当没有窗口在使用当前版本的worker时在后台激活)

当然如果作为严格划分Register的存在更加像是一个动作,所以实际上它的整个完整的执行链路更加像是(作者自己总结):

image.png

ServiceWorkerGlobalScope

接着我们来看一下,Service Worker内部的全局对象ServiceWorkerGlobalScope是什么样的。首先ServiceWorkerGlobalScope的继承关系如下:

image.png

根据继承关系,我们直接来了解ServiceWorkerGlobalScope上新增的方法/属性/事件即可

属性

其中CacheStorage在离线缓存中有着较大的比重,所以我们这里着重看看CacheStorage

CacheStorage

CacheStorage实际上是Cache对象群的接口代表(其实可以理解为存在多个不同的存储空间),那么实际上Cache才是真正的存储对象类型,他可以通过cacheName映射到对应的Cache对象,CacheStorage包含以下方法(来自MDN):

方法

检查给定的 Request 是否是 CacheStorage 对象跟踪的任何 Cache 对象的键,并返回一个resolve为该匹配的 Promise .(所以这里存在如果当一个相同的Request存在于不同的Cache中,可能命中的Cache并非是理想的数据)

如果存在与 cacheName 匹配的 Cache 对象,则返回一个resolve为true的 Promise .

返回一个 Promise ,resolve为匹配  cacheName (如果不存在则创建一个新的cache)的 Cache 对象

查找匹配 cacheName 的 Cache 对象,如果找到,则删除 Cache 对象并返回一个resolve为true的 Promise 。如果没有找到 Cache 对象,则返回 false.

返回一个 Promise ,它将使用一个包含与 CacheStorage 追踪的所有命名 Cache 对象对应字符串的数组来resolve. 使用该方法迭代所有 Cache 对象的列表。

了解完CacheStorage我们再来看看Cache

Cache

Cache 接口为缓存的 Request / Response  对象对提供存储机制,例如,作为ServiceWorker 生命周期的一部分。请注意,Cache 接口像 workers 一样,是暴露在 window 作用域下的。尽管它被定义在 service worker 的标准中,  但是它不必一定要配合 service worker 使用(实际上我们在window的上下文中也可以根据caches对象访问到具体的cache).

一个域可以有多个命名 Cache 对象。你需要在你的脚本 (例如,在 ServiceWorker 中)中处理缓存更新的方式。除非明确地更新缓存,否则缓存将不会被更新;除非删除,否则缓存数据不会过期。使用 CacheStorage.open(cacheName) 打开一个Cache 对象,再使用 Cache 对象的方法去处理缓存.

你需要定期地清理缓存条目,因为每个浏览器都硬性限制了一个域下缓存数据的大小。缓存配额使用估算值,可以使用 StorageEstimate API 获得(可以通过navigator.storage (en-US)WorkerNavigator.storage (en-US)进行访问)。浏览器尽其所能去管理磁盘空间,但它有可能删除一个域下的缓存数据。浏览器要么自动删除特定域的全部缓存,要么全部保留。确保按名称安装版本缓存,并仅从可以安全操作的脚本版本中使用缓存。

方法

返回一个 Promise对象,resolve的结果是跟 Cache 对象匹配的第一个已经缓存的请求。

返回一个Promise 对象,resolve的结果是跟Cache对象匹配的所有请求组成的数组。

抓取这个URL, 检索并把返回的response对象添加到给定的Cache对象.这在功能上等同于调用 fetch(), 然后使用 Cache.put() 将response添加到cache中.

抓取一个URL数组,检索并把返回的response对象添加到给定的Cache对象。

同时抓取一个请求及其响应,并将其添加到给定的cache。

搜索key值为request的Cache 条目。如果找到,则删除该Cache 条目,并且返回一个resolve为true的Promise对象;如果未找到,则返回一个resolve为false的Promise对象。

返回一个Promise对象,resolve的结果是Cache对象key值组成的数组。

事件

  • activate

当前sw被激活时触发当前事件

  • fetch

当页面发起网络请求时触发当前事件(其中包括各种静态资源拉取)

  • install

安装发生的生命周期

  • message

消息事件

  • notificationclick

消息通知点击事件

  • notificationclose

消息通知关闭事件

  • push

当收到服务器的推送通知时

  • pushsubscriptionchange

推送订阅即将失效或者已经失效时触发

  • sync

当从 Service Worker 客户端页面调用 SyncManager.register 时会触发此事件。如果网络可用,则立即尝试同步,或者在网络可用时立即尝试同步。

方法

OK 那么到这里我们就基本了解完我们当前的Serview Worker的API

我们来尝试编写一个小DEMO吧

案例

首先我们要知道Service worker仅支持在HTTPS下使用,但是如果是localhost进行访问是可以在本地的浏览器环境进行调试的

<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8">
  <title>web worker api</title>
  <style>
  </style>
</head>

<body>
  <div>
    i am page
  </div>
  <script>
   navigator.serviceWorker.register('sw.js', {
    scope: './'
  }).then(function(reg) {
    // registration worked
    console.log('Registration succeeded. Scope is ' + reg.scope);
  }).catch(function(error) {
    // registration failed
    console.log('Registration failed with ' + error);
  });
  </script>
</body>

</html>
// sw.js
const cacheVersion = 'v1'
self.addEventListener('install', function (event) {
  // 指定的安装工作,当前install需要等待waitUntil中的promise resolve后才会进入等待激活阶段waiting
  event.waitUntil(
    caches.open(cacheVersion).then(function (cache) {
      return cache.addAll([
        '/serviceWorkerStudy/index.html'
      ])
    })
  )
})
self.addEventListener('fetch', function (event) {
  event.respondWith(
    caches.match(event.request).then(function (resp) {
      return resp || fetch(event.request).then(function (response) {
        return caches.open(cacheVersion).then(function (cache) {
          // 不缓存chrome
          if (!event.request.url.includes('chrome-extension')) {
            cache.put(event.request, response.clone())
          }
          return response
        })
      })
    })
  )
})

self.addEventListener('activate', function (event) {
  const cacheWhitelist = [cacheVersion]
  event.waitUntil(
    caches.keys().then(function (keyList) {
      return Promise.all(
        keyList.map((key) => {
          if (cacheWhitelist.indexOf(key) === -1) {
            return caches.delete(key)
          }
          return null
        }).filter(d => !!d)
      )
    })
  )
})

然后我们通过localhost:xxxx/xxx.html进行访问,接着我们打开Chrome DevTools打开查看我们的网络请求此时大概如下所示:

image.png

可以看到我们大小还是存在的,也就是说还是实际发起了网络请求的,那么我们打开应用(aplication)来看看serview worker的一个运行情况

image.png

我们会发现由于我们是首次注册service worker 我们的worker已经进入到了激活阶段。 OK,那么我们修改一下html中的内容将i am page改成i am hidden然后我们刷新一下浏览器。我们会发现页面依旧会显示i am page, 我们查看一下网络

image.png

此时我们会发现我们首页已经被缓存下来了,同时我们可以打开我们的应用(aplication)查看缓存空间我们可以查看的到我们已经被缓存的静态资源文件

image.png

OK 那么这里简单的静态资源缓存就结束了,如果大家要问如何刷新缓存呢? 可以修改一下在sw.js文件中的cacheVersion重新刷新两次页面就可以了,为什么需要两次呢,这就是课后作业。

那么关于worker的内容我们就学习到这里,如果有更多的补充欢迎留言

完美!

下一章:面试准备-浏览器存储