介绍 js 的 Workers 前, 先思考什么是异步javascript? 为什么需要异步javascript的存在?
我们知道在编程模型上分为同步编程和异步编程:
同步编程即各任务按顺序一个一个执行, 前一个任务完全执行完后再执行下一个任务, 程序执行顺序跟编写的顺序是一致的, 逻辑比较清晰, 但遇到有耗时任务时容易产生阻塞.
异步编程即各任务不一定是按顺序执行的, 对于耗时的任务可以处理成异步任务, 异步任务开启后, 不等待执行结果就可以执行下一个任务, 对其他事件做出响应. 异步任务执行完后通过回调函数的方式将结果返回. 异步模式有很多, 例如setTimeout、ajax、fetch、getUserMedia、Promise、async/await等.
因为javascript是单线程的(注意浏览器不是单线程的, js调用其内部的api也不一定是单线程的, 如定时器), 其只有一个线程用来执行代码, 所以为了避免遇到计算量大、耗时的任务阻塞线程继续往下执行, js引入了事件循环的异步编程机制, 解决同步单线程的阻塞问题.
虽然有事件循环机制, 但其本质上还是在一个单线程上执行, 它在同一时间也只能做一件事情, 如果它正在等待长期运行的同步调用返回,就不能做其他任何事情. 有没有一种方法, 可以在多线程中并行执行某些任务? Workers 就赋予了在不同线程中运行某些任务的能力,因此你可以启动任务,然后继续其他的处理.
当然对于js的多线程的代码来说, 主线程代码和 Worker 线程代码是运行在完全分离的环境中,他们不能直接访问彼此的变量, 只能通过相互发送消息来进行交互. 因此 Workers 是不能访问 DOM(窗口、文档、页面元素等等)的.
通过使用 Web Workers,Web 应用程序可以在独立于主线程的后台线程中,运行一个脚本操作。这样做的好处是可以在独立线程中执行费时的处理任务,从而允许主线程(通常是 UI 线程)不会因此被阻塞/放慢[MDN解释].
js中的Web Workers有三种类型:
通常所说的 Worker 是指Deicated Workers, 其接口是 Web Workers API 的一部分, 他可以由脚本创建后台任务, 在任务执行的过程中, 可以向其他创建者收发信息, 我们可以直接使用Web Workers API 的 Worker 构造函数创建实例, 所有Worker必须与其创建者同源.
下面示例包含Worker的基本API, postMessage、onmessage、onmessageerror、terminate, 配合index.html, 直接在浏览器中启动即可看到效果
// 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.');
}
// worker.js, Worker线程接收主线程信息
onmessage = (e) => { postMessage('Worker Start'); };
整体的使用方式比较简单, 直接 new Worker 创建新的 Worker 线程, 执行 worker 的代码, 如果 worker 中执行计算密集型的耗时代码, 则不影响主线程的执行.
之前说到js中的主线程和 worker 线程是隔离的, 他们的变量是不能共用了, 只能通过 postMessage 进行消息传递, 其本质是 Worker 运行在另一个全局上下文中, 有自己的作用域, 与当前的 window 不同, 也无法直接访问Window对象. 但是 Web Workers API 提供了接口 WorkerGlobalScope 来访问一些Web API, 每个 WorkerGlobalScope 也都有自己的事件循环. 对于 Dedicated Workers 来说, 在 Worker 线程内提供了 DedicatedWorkerGlobalScope 对象, 他继承了 WorkerGlobalScope 属性, 可以通过Self来访问, 例如 self.location 会输出:
其也是 Web Workers API 的一种共享线程, 说他共享是因为他可以从几个浏览上下文中访问, 例如几个窗口、iframe 或其他 worker. 对于同一个 worker url 只会创建一个 SharedWorker, 其他页面再使用同样的 url 创建 SharedWorker,会复用已创建的 worker,这个worker由那几个浏览上下文共享. 也因为他是可以跨窗口访问, 因此 SharedWorker 是通过活动端口 port 来发送和接收消息.
单页面的Worker线程和主线程之间的通信与 Dedicated Workers 类似, 只不过是调用 SharedWorker 对象进行实例化, 这里不做举例. 下面主要对如何使用 SharedWorker 是进行多页面通信示例, 这里创建两个html页面:
// 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; }
}
// 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; }
}
// 共享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);
})
};
};
分别运行两个页面后(保证同源), 会看到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 的广播计算结果
与 Dedicated Workers 一样, Worker 线程内提供了 SharedWorkerGlobalScope 对象, 他继承了 WorkerGlobalScope 属性, 同样可以通过 self 来访问, 如 self.performance 会输出:
简称SW, 一般作为 web 应用程序、浏览器和网络(如果可用)之间的代理服务. 他们旨在(除开其他方面)创建有效的离线体验, 拦截网络请求, 以及根据网络是否可用采取合适的行动, 更新驻留在服务器上的资源. 他们还将允许访问推送通知和后台同步 API.[MDN解释]
简单理解, 其实就是有一个独立于当前网页线程的后台线程, 在网页发起请求时进行代理,并缓存相关文件, 以便用户可以进行离线访问. SW 也是 PWA(渐进式网页应用) 的重要组层部分, 许多技术框架(如React、Vue)会默认带上该功能.
由于 SW 会作为代理服务出现, 并且会去拦截网络请求, 为避免中间人攻击和考虑到其他安全因素, 必须使用HTTPS来进行页面访问, 如果是本地开发, localhost也被认为是安全的.
有些支持 SW 的浏览器版本可能默认开启一些特性, 导致无法正常运行 SW, 可以进行响应的配置, 例如
SW的调用可以拆分为以下几个阶段, 也即生命周期
在主线程中进行 SW 注册, 首次注册使用 navigator.serviceWorker.register 方法, 创建一个给定 scriptURL 的 ServiceWorkerRegistration, 调用时会立刻去下载对应的 scriptURL 文件, 代码如下, 其中scope 表示 SW 可以控制的 URL 范围. 通常是基于当前的 location来解析传入的路径.
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 全局上下文.
注册和下载之后浏览器会进入安装阶段, 可以通过 install 事件进行监听, 并且在这个事件里可以对站点资源进行缓存
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open('v1').then((cache) => cache.addAll([
'/sw/',
'/sw/index.html',
...
]))
);
});
安装完成后, 会接收到一个激活事件, 在该事件中可以进行一些缓存的清理工作
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 条目.
当重新进入 SW 页面, 或者在 SW 上的一个事件被触发并且过去 24 小时没有被下载时会触发更新, 如果下载的 SW 文件是新的, 安装就会在后台尝试进行, 安装成功后不会被激活, 会进入 waiting 阶段, 直到所有已加载的页面不再使用旧的 SW 才会被激活.
还有一个值得监听的重要事件是 fetch, 他是进行自定义请求响应的, 每次请求被 SW 控制的资源时,都会触发 fetch 事件,这些资源包括了指定的 scope 内的文档, 和这些文档内引用的其他任何资源. 可以在该监听事件中做一些操作, 比如将请求资源写入缓存、控制资源获取优先级等. event.respondWith 正好能为我们劫持 HTTP 请求来执行自己方法.
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',
})
);
});
当访问页面时, 会去拉取对应的资源, 通过Network、Application、chrome://inspect/#service-workers, 可以查看相应的状态
首先查看NetWork, 可以看到部分资源已经从 SW 的缓存中获取, 此时将网络断开, 发现缓存的资源仍然可以获取到, 页面仍然可以正常访问
再看看Application的Cache Storage, 可以看到以 key 值 v1 存储的响应缓存, 这些缓存文件都是我们在 install 中添加到我们待缓存的列表中的文件路径
在 Application 的 Service Workers 中可以看到对应 SW的一些状态记录, 以及可以对其进行相应的操作
同样使用 chrome 开发者工具, 可以查看 SW 线程的一些相关信息, 以及终止 SW 线程
SW 功能强大, 不仅可以用作网页的离线访问, 还有很多其他的用途, 也有很多三方库的封装, 例如 Workbox, SW 还可以运用于:
……
在 js 的单线程运行环境外加时间循环机制的加持下, 我们可以比较方便处理我们的一些同步和异步逻辑, 不过有时面对计算密集型、耗时高、性能要求高、网络环境差等场景下, 我们可以使用更为有效的 Web Workers 开辟多线程去进行一些优化. 其实除了 Web Workers 中的多线程, Nodejs中同样也有相应的多线程处理方式, 可见多线程的作用之大. 而 Web Workers 除了上面说的三种类型, 还包括音频 Workers、Chrome Workers 等等, 也都在特定的场景中非常有用.
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。