深入web workers (上)
前段时间,为了优化某个有点复杂的功能,我采用了shared workers + indexDB,构建了一个高性能的多页面共享的服务。由于是第一次真正意义上的运用workers,比以前单纯的学习有更多体会,所以这里就分享出来!
各种worker概要
有三种worker:普通的worker、shared worker、service worker。(有极少的文档说有四种,多了一个 audio worker,但其实所谓的audio worker 就是 audio context,用于构建强大的音/视频处理系统)
- 普通worker,也叫专用worker,仅能被生成它的脚本所使用,全局对象this是DedicatedWorkerGlobalScope对象
- 共享worker,即sharedworker,能被不同的window页面,iframe,以及worker访问(当然要遵循同源限制),全局对象this是 SharedWorkerGlobalScope 对象。
- serviceWorker,专为PWA应用而生的worker,构建一个PWA必须要基于https,且所使用的密钥签名必须是经过CA认证的,否则你的浏览器都将认为不安全,而不会加载你的service worker。由于这个特殊性,我并没有深入了解service worker!
serviceWorker 一般作为web应用程序、浏览器和网络(如果可用)之前的代理服务器。它们旨在(除开其他方面)创建有效的离线体验,拦截网络请求,以及根据网络是否可用采取合适的行动并更新驻留在服务器上的资源。他们还将允许访问推送通知和后台同步API。
作为官方标准,3种worker当前的浏览器支持性都非常良好,可以放心使用! 呃,等一下,shared worker的支持性好像不太好哟:
不用紧张,不支持的主要是应用场景不多的移动端(移动端应用谁会开启多窗口?)和ios了,总体可以忽略(如果必须考虑ios的web端,那就要考虑回退方案了)。
如果你要实现的功能中,用户多窗口操作是很正常的;有数据库(如indexDB)、socket等链接;大量相同的可共用的变量……毫无疑问你应该使用shared worker!
我所要优化的功能就有这些特点,这就是采用shared worker的原因。
worker与主线程的交互
这里只讲专用worker 和 sharedWorker两种(service worker没有深入了解)。专用worker和sharedWorker差别很小,所以接下来先详细的把专用worker讲解清楚,再讲解sharedWorker的不同点。
专用worker和主线程的交互
示例:
- // 主线程:
- const worker = new Worker('./worker.js')
- worker.onmessage = (e) => {
- console.log('[main receive]:',e.data )
- }
- worker.postMessage('Hello ,this is main thread')
- // worker.js:
- addEventListener('message', function (e) {
- console.log('[worker receive]:', e.data )
- postMessage('Hi,this is worker thread')
- });
- 主线程和worker 都是通过 postMessage 方法向对方发送消息。
- 双方也都是通过监听 message 事件来接收消息(上面分别有两种监听方法: addEventListener 和 onmessage ,就是个DOM Event )。
- 事件句柄的data字段的值就是发送消息时传递的内容。
运行结果:
postMessage发送 + 监听message事件接收——交互原理就这么简单,这也是唯一的交互方式!
深入消息的数据传递
数据绝对不会以引用的方式“共享”过去,要么被复制,要么被转移
拷贝
普通的数据传递,是通过拷贝来进行的。也就是发过去的是一份拷贝而非引用,如果是个对象,那么修改对象属性是互不影响的——数据能独立变化,互不影响。
和indexDB一样,拷贝是采用结构化克隆的规范的,经过测试它至少有以下副作用:
- 对象里不能含有方法,也不能拷贝方法
- 对象里不能含有symbol,也不能拷贝symbol,键为symbol的属性会被忽略
- 大多数对象的类信息会丢失。如:传递一个 obj=new Person() 收到的数据将没有 Person这个类信息。
但是如果是一个内置对象,如Number,Boolean这样的对象,则不会丢失!(注意:这一点和mdn描述的不一样) - 不可枚举的属性(enumerable为false)将会被忽略。
- 属性的可写性配置(writable配置)将丢失。
- 经过测试,所有通过 Object.defineProperties 新增的(注意 是新增的!)属性都将被忽略。
转移
拷贝在某些情况下会存在性能问题,比如拷贝一个500M的文件,肯定会花较多时间。除了拷贝还提供通过转移的方式来传递数据。
目前只有4种对象支持转移:ArrayBuffer, MessagePort, ImageBitmap和 OffscreenCanvas。
ArrayBuffer是原始的二进制缓冲区,文件File,Blob,各种 TypedArray ,都是基于arrayBuffer的。接下来以ArrayBuffer来举例说明转移传递数据:
可以转移的数据,也可以通过拷贝来传递:
- 1 // 主线程:
- 2 const worker = new Worker('./worker.js')
- 3 const u8 = new Uint8Array(new ArrayBuffer(1)) // 创建一个长度为1的TypedArray u8
- 4 u8[0] = 1
- 5 worker.onmessage = (e) => {
- 6 const receive = e.data
- 7 console.log('[main receive]:', receive, 'orginal:', u8)
- 8 }
- 9 worker.postMessage(u8) // 通过普通的拷贝,将u8传给worker
- 10
- 11
- 12 // worker.js :
- 13 addEventListener('message', function (e) {
- 14 const receive = e.data
- 15 receive[0] = 9 // worker 收到u8后,改变里面的内容
- 16 console.log('[worker change]:',receive)
- 17 postMessage(receive)
- 18 });
console打印结果:
这个例子仅仅表明,可以转移的bufferArray也可以通过拷贝传递。注意看第二条打印:和预想中的一样,主线程和worker线程的数据会独立变化。
转移传递示例:
转移很简单,仅仅是在postMessage时,额外传入第二参数,表明要转移的对象,将上面例子稍加改造:
- 1 // 主线程:
- 2 const worker = new Worker('./worker.js')
- 3 const u8 = new Uint8Array(new ArrayBuffer(1))
- 4 u8[0] = 1
- 5 worker.onmessage = (e) => {
- 6 const receive = e.data
- 7 console.log('[main receive]:', receive, 'orginal:', u8)
- 8 worker.postMessage('finish')
- 9 }
- 10 worker.postMessage(u8 , [u8.buffer]) // 第二个参数表示要转移的对象:注意这必须是一个数组;注意转移的是typedArray的buffer,而不是typedArray!
- 11
- 12
- 13
- 14 // worker.js :
- 15 let receive
- 16 addEventListener('message', function (e) {
- 17 if(e.data==='finish'){
- 18 console.log('[worker after transfer]',receive)
- 19 return;
- 20 }
- 21 receive = e.data
- 22 receive[0] = 9
- 23 console.log('[worker change]:',receive)
- 24 postMessage(receive,[receive.buffer]) // 转移typedArray的buffer,typedArray长度将变成0!
- 25
- 26 }, false);
console的打印结果(注意理解两个空的typedArray,为什么是空的数组,因为buffer的“使用权”被转移了!):
把二进制数据直接转移给子线程,一旦转移,主线程就无法再使用这些二进制数据了!
sharedWorker与专用worker的差异
消息交互的差异:
sharedWorker与主线程交互和专用worker基本一样,只是多了一个port:
- 1 // 主线程:
- 2 const worker = new SharedWorker('worker.js', { name: '公共服务' })
- 3 // 创建worker时,除了文件路径,还可以传入一些额外的配置:如name。
- 4 // worker的name有id的功能,不同页面要想共享sharedWorker,名称相同是必要条件!
- 5 const key = Math.random().toString(16).substring(2)
- 6 worker.port.postMessage(key) // 通过worker.port发送消息
- 7 worker.port.onmessage = e => { // 通过worker.port接收消息
- 8 console.log(e.data)
- 9 }
- 10
- 11
- 12 // worker.js:
- 13 const buf = []
- 14 onconnect = function (evt) { // 当其他线程创建sharedWorker其实是向sharedWorker发了一个链接,worker会收到一个connect事件
- 15 const port = evt.ports[0] // connect事件的句柄中evt.ports[0]是非常重要的对象port,用来向对应线程发送消息和接收对应线程的消息
- 16 port.onmessage = (m) => {
- 17 buf.push(m.data)
- 18 console.log(buf) // 这个打印没看到?请看调试差异小节!
- 19 port.postMessage('worker receive:' + m.data)
- 20 }
- 21 }
注意看上面的注释,信息交互都是通过port进行!通常一个sharedWorker可以对应多个主线程,所以sharedWorker多了一个connect事件,通过这个事件获取各自的port与各自的主线程通信!
需要注意的是,在sharedWorker中,如果不是通过onmessage 而是通过addEventListener监听message来接收消息,必须显式调用start开启连接,否则将无法收到消息,只能发送消息。示例:
- // sharedWorker内部:
- port.start()
- port.addEventListener('message',e=>{
- // ...
- })
- // 主线程内部:
- worker.port.start()
- worker.port.addEventListener('message',e=>{
- // ...
- })
调试的差异:
在上方的例子有两处打印,第8行 主线程打印worker传过来的消息,第18行worker内部打印缓存下来的[主线程传过来的]消息。奇怪的是,当你打开开发者工具,在Console中并没有看到第18行的打印信息!
要想看到第18行打印的信息对sharedWorker进行调试,需要进行下面两步:
启动一个新的标签页,网址输入:chrome://inspect/#workers 界面如下:
点击 inspect(千万不要点击terminate,这个是结束worker的),你会看到浏览器会打开一个新窗口,新窗口的界面就是开发者工具界面(做过web移动端开发的应该很熟悉这个界面):
切换到Sources页面,就可以对SharedWorker代码进行调试了!
全局对象差异:
在主线程中,一切都很好理解,我们通过创建的worker来监听或发送消息,但在worker内部,则会发现直接调用 postMessage、onmessage等方法。
这是因为在worker内部,有一个全局对象 self,相当于globalThis(如果支持的话),相当于全局作用域下的this,直接调用相当于 self. 调用:
- // 专用worker示例:
- globalThis.addEventListener('message', function (e) {})
- self.postMessage(msgObj)
- // serviceWorker 示例:
- // 顶级作用域:
- this.onconnect = function(evt){}
上面的globalThis,self,this 均可以省略,类似于主线程的window!
正像前面提到过的:专用worker全局对象this是DedicatedWorkerGlobalScope对象,sharedWorker则是SharedWorkerGlobalScope 对象,这两者都是WorkerGlobalScope的派生类,所以可以这样判断:
- console.log(this instanceof DedicatedWorkerGlobalScope) // 专用worker 中 true, sharedWorker和主线程中报异常错误
- console.log(this instanceof SharedWorkerGlobalScope) // sharedWorker 中 true, 专用worker和主线程中报异常错误
- console.log(this instanceof WorkerGlobalScope) // 专用worker和sharedWorker中都是true, 主线程中异常错误
线程生命周期差异:
专用worker很好理解:每打开一个页面就创建一个worker线程,关闭页面worker就销毁,刷新一次页面worker就经历了一次销毁和创建的过程,不同页面互不干扰。
你也可以像下面这样主动销毁一个worker:
- // 专用worker内部
- self.close() // 主动关闭worker连接,后续发送消息将静默失败
- // 外部主线程:
- worker.terminate() // 或者外部这样关闭连接,注意:一旦关闭worker,worker将会被销毁,worker内的所有进行中的任务(如定时任务)都将直接销毁
一个sharedWorker可以对应多个主线程,所以:打开页面时,如果没有sharedWorker时才创建,否则就共用已经存在的sharedWorker;当只有当前页面和sharedWorker连接时,关闭当前页面sharedWorker才会被销毁,刷新当前页面sharedWorker才会先销毁后创建。
sharedWorker的连接也可以主动断开,但仅仅是断开链接,并不会销毁sharedWorker,即便是唯一使用sharedWorker的页面断开了链接。worker内部进行中的任务会正常进行,只是不能正常与主线程通信了!
- // 主线程:
- worker.port.close() // 仅仅关闭连接
- // worker内部(拿到port后):
- port.close() // 仅仅关闭连接
很多人喜欢像下面这样写代码,但请注意注释中的说明,:
- const clients = new Set() // 用于记录所有与worker连接的线程
- this.onconnect = function (c) {
- let port = c.ports[0]
- clients.add(port) // 没有任何方法知道 port 已经断开链接了(如页面关闭),所以clients只能无限添加port。这会引起内存泄露
- // 在你不得不这么做,以实现诸如“向所有页面发送消息”的需求时,注意控制内存泄露的幅度:
- // 所有port使用同一个onmessageHandler实例和onmessageerrorHandler实例,是个不错的选择!
- port.onmessage = onmessageHandler
- port.onmessageerror = onmessageerrorHandler
- }
- function onmessageHandler(evt){}
- function onmessageerrorHandler(evt){}
事件和异常的交互
在面多异常和事件相关的问题时,你必须明白:worker 和 主线程是两个线程!那么就很好理解:
worker中的事件,主线程是没法监听到的,反之亦然;worker中的异常,主线程是无法感知的,反之亦然!再次强调,二者唯一的交互方式就是 postMessage和监听message事件。
- // worker.js内部:
- // ... other code
- throw new Error('test error')
- // 这个错误无法被主线程获取,相反 你会在worker的console中看到“错误未捕获提示”的错误提示,而不是主线程的console!
主线程中可以监听worker的error事件,但请注意这到底是什么error:
- worker.onerror = e=>{
- // 请注意 这里主线程监听的是创建worker时的异常,而非worker创建成功后内部运行的异常
- // 创建时异常:如下载worker脚本错误,路径错误,worker脚本解析错误等
- }
两边都能监听 messageerror 事件,但是经过测试一直都没法触发这个事件,按官方的解释是:当接收到一个消息,但是消息的数据无法成功解析时,会触发这个事件。请注意,这里是“接收”!我尝试发送一个无法拷贝的对象(如含有function字段),但是在发送时就失败了。
可以看到 onerror 和 onmessageerror事件都是和对方无关的事件!
结语
本文深入讲解了 worker 和 sharedWorker 与 主线程的交互。
现在你已经能用两种worker做一些简单的工作了,但是在面临较复杂的工作,以及在面临webpack这样的工程中,使用worker(或sharedWorker)会面临新的问题。敬请期待:深入web workers (下),我将与你详细探讨workers在工程化中的最佳实践。
深入web workers (上)的更多相关文章
- Web Workers
在 Web Workers 中使用 postMessage 和 onmessage 首先,需要在客户端页面的 JavaScript 代码中 new 一个 Worker 实例出来,参数是需要在另一个线程 ...
- html5 Web Workers
虽然在JavaScript中有setInterval和setTimeout函数使javaScript看起来好像使多线程执行,单实际上JavaScript使单线程的,一次只能做一件事情(关于JavaSc ...
- 3D拓扑自动布局之Web Workers篇
2D拓扑的应用在电信网管和电力SCADA领域早已习以为常了,随着OpenGL特别是WebGL技术的普及,3D方式的数据可视化也慢慢从佛殿神堂步入了寻常百姓家,似乎和最近高档会所被整改为普通茶馆是一样的 ...
- HTML5 Web Workers来加速您的移动Web应用
一直以来,Web 应用程序被局限在一个单线程世界中.这的确限制了开发人员在他们的代码中的作为,因为任何太复杂的东西都存在冻结应用程序 UI 的风险.通过将多线程引入 Web 应用程… 在本文中,您将使 ...
- (92)Wangdao.com_第二十五天_线程机制_H5 Web Workers 分线程任务_事件 Event
浏览器内核 支撑浏览器运行的最核心的程序 IE 浏览器内核 Trident内核,也是俗称的IE内核Chrome 浏览器内核 统称为 Chromium 内核或 ...
- 通过使用Web Workers,Web应用程序可以在独立于主线程的后台线程中,运行一个脚本操作。这样做的好处是可以在独立线程中执行费时的处理任务,从而允许主线程(通常是UI线程)不会因此被阻塞/放慢。
Web Workers API - Web API 接口参考 | MDNhttps://developer.mozilla.org/zh-CN/docs/Web/API/Web_Workers_API ...
- JavaScript是如何工作的:Web Workers的构建块 + 5个使用他们的场景
摘要: 理解Web Workers. 原文:JavaScript是如何工作的:Web Workers的构建块 + 5个使用他们的场景 作者:前端小智 Fundebug经授权转载,版权归原作者所有. 这 ...
- WijmoJS 使用Web Workers技术,让前端 PDF 导出效率更高效
概述 Web Workers是一种Web标准技术,允许在后台线程中执行脚本处理. WijmoJS 的2018v3版本引入了Web Workers技术,以便在生成PDF时提高应用程序的运行速度. 一般来 ...
- Web Workers文档
Web Worker为Web内容在后台线程中运行脚本提供了一种简单的方法.线程可以执行任务而不干扰用户界面.此外,他们可以使用XMLHttpRequest执行 I/O (尽管responseXML和 ...
随机推荐
- 【漏洞复现】S2-052 (CVE-2017-9805)
一.漏洞描述 Struts2 的REST插件,如果带有XStream组件,那么在进行反序列化XML请求时,存在未对数据内容进行有效验证的安全隐患,可能发生远程命令执行. 二.受影响版本 Struts2 ...
- RHSA-2017:1931-中危: bash 安全和BUG修复更新(代码执行)
[root@localhost ~]# cat /etc/redhat-release CentOS Linux release 7.2.1511 (Core) 修复命令: 使用root账号登陆She ...
- GDB 调试 .NET 程序实录 - .NET 调用 .so 出现问题怎么解决
注:本文重要信息使用 *** 屏蔽关键字. 最近国庆前,项目碰到一个很麻烦的问题,这个问题让我们加班到凌晨三点. 大概背景: 客户给了一些 C语言 写的 SDK 库,这些库打包成 .so 文件,然后我 ...
- 制作西北地区地图数据并maskout
1.从全国地图数据中选中西北5省:打开bou2_4p.shp文件添加相应的图层(中国各省的行政区域),选中工具栏中的"通过矩形选择要素"工具,用鼠标点击选择要输出的图元,按住ctr ...
- day40 Pyhton 并发编程03
一.内容回顾 进程是计算机中最小的资源分配单位 进程与进程之间数据隔离,执行过程异步 为什么会出现进程的概念? 为了合理利用cpu,提高用户体验 多个进程是可以同时利用多个cpu的,可以实现并行的效果 ...
- filebeat7.5 日志
百度网盘 提取码: 6cvu 解压 tar -zxvf filebeat-7.5.0-linux-x86_64.tar.gz mv filebeat-7.5.0-linux-x86_64 /usr/l ...
- python 保存登录状态 cookie
import requests from lxml import etree import faker url = "https://www.yeves.cn/admin/Articles& ...
- xpath教程-通过ID和Class检索 转
通过ID和Class检索 必备知识点 在html中,id是唯一的 在html中,class是可以多处引用的 工具 Python3版本 lxml库[优点是解析快] HTML代码块[从网络中获取或者自 ...
- C# 微支付退款查询接口 V3.3.6
#region 微支付退款查询 string Nonce = CreateRandomCode(15).ToLower(); //生成15个随机字符string sign1 = "appid ...
- RocketMQ主从搭建
RocketMQ可分为以下几种模式: 单点模式 主从模式 双从模式 双主双从模式,多主多从模式 搭建主从模式 tar -zxvf rocketmq-4.6.0.tar.gz -C /usr/local ...