前言

JavaScript从应用开初就始终基于事件循环的单线程运行模型,即便是胜利进军后端开发的Nodejs也没有扭转这一模型。那么对于计算密集型的利用,咱们必须创立新过程来执行运算,而后执行过程间通信实现传参和获取运算后果。否则会造成UI界面卡顿,甚至导致浏览器无响应。
从性能实现来看,咱们能够通过新增iframe加载同域页面来创立JSVM过程执行运算从而防止造成界面卡顿的问题。但存在如下问题:

  1. 这里波及到HTML页面、JavaScript、iframe同源策略、iframe间音讯通信的综合利用,其中理论的运算逻辑都以JavaScript形容,而HTML页面和iframe同源策略属于底层基础设施,而且这些基础设施没方法封装为一个类库对外提供服务,这就增大利用开发和运维的难度;
  2. 过程的创立和销毁老本相对比线程的创立和销毁多得多。

侥幸的是HTML5为JavaScript引入多线程运行模型,这也是本文将和大家一起探讨的———Web Worker。

困在笼子里的Web Worker

在应用Web Worker前咱们要理解它的能力边界,让咱们防止无谓的撞壁:

  1. 同源限度

1.1. 以http(s)://协定加载给WebWorker线程运行的脚本时,其URL必须和UI线程所属页面的URL同源;
1.2. 不能加载客户端本地脚本给WebWorker线程运行(即采纳file://协定),即便UI线程所属页面也是本地页面;

  1. DOM和BOM限度

1.1. 无法访问UI线程所属页面的任何DOM元素;
1.2. 可拜访如下BOM元素
1.2.1. XMLHttpRequest/fetch
1.2.2. setTimeout/clearTimeout
1.2.3. setInterval/clearInterval
1.2.4. location,留神该location指向的是WebWorker创立时以UI线程所属页面的以后Location为根底创立的WorkerLocation对象,即便尔后页面被屡次重定向,该location的信息仍然放弃不变。
1.2.5. navigator,留神该navigator指向的是WebWorker创立时以UI线程所属页面的以后Navigator为根底创立的WorkerNavigator对象,即便尔后页面被屡次重定向,该navigator的信息仍然放弃不变。

  1. 通信限度,UI线程和WebWorker线程间必须通过音讯机制进行通信。

Dedicated Web Worker详解

Web Worker分为Dedicated Web Worker和Shared Web Worker两类,它们的个性如下:

  1. Dedicated Web Worker仅为创立它的JSVM过程服务,当其所属的JSVM过程完结该Dedicated Web Worker线程也将完结;
  2. Shared Web Worker为创立它的JSVM过程所属页面的域名服务,当该域名下的所有JSVM过程均完结时该Shared Web Worker线程才会完结。

根本应用

  1. UI线程
const worker = new Worker('work.js') // 若下载失败如404,则会默默地失败不会抛异样,即无奈通过try/catch捕捉。                                                                                                                      const workerWithName = new Worker('work.js', {name: 'worker2'}) // 为Worker线程命名,那么在Worker线程内的代码可通过 self.name 获取该名称。                                                                                                                                                                                                                                                                                                                                      worker.postMessage('Send message to worker.') // 发送文本音讯                                                                                                                                                                     worker.postMessage({type: 'message', payload: ['hi']}) // 发送JavaScript对象,会先执行序列化为JSON文本音讯再发送,而后在接收端主动反序列化为JavaScript对象。                                                                      const uInt8Array = new Uint8Array(new ArrayBuffer(10))                                                                                                                                                                            for (let i = 0; i < uint8array.length; ++i) {                                                                                                                                                                                       uInt8Array[i] = i * 2                                                                                                                                                                                                         }                                                                                                                                                                                                                                 worker.postMessage(uInt8Array) // 以先序列化后反序列化的形式发送二进制数据,发送后主线程依然能拜访uInt8Array变量的数据,但会造成性能问题。                                                                                        worker.postMessage(uInt8Array, [uInt8Array]) // 以Transferable Objets的形式发送二进制数据,发送后主线程无法访问uInt8Array变量的数据,但不会造成性能问题,适宜用于影像、声音和3D等大文件运算。                                                                                                                                                                                                                                                                                   // 接管worker线程向主线程发送的音讯                                                                                                                                                                                               worker.onmessage = event => {                                                                                                                                                                                                       console.log(event.data)                                                                                                                                                                                                       }                                                                                                                                                                                                                                 worker.addEventListener('message', event => {                                                                                                                                                                                       console.log(event.data)                                                                                                                                                                                                       })                                                                                                                                                                                                                                                                                                                                                                                                                                                                              // 当发送的音讯序列化失败时触发该事件。                                                                                                                                                                                           worker.onmessageerror = error => console.error(error)                                                                                                                                                                                                                                                                                                                                                                                                                           // 捕捉Worker线程产生的异样                                                                                                                                                                                                       worker.onerror = error => {                                                                                                                                                                                                         console.error(error)                                                                                                                                                                                                          }                                                                                                                                                                                                                                                                                                                                                                                                                                                                               // 敞开worker线程                                                                                                                                                                                                                 worker.terminate()                                                                                                                                                                                                                
  1. Worker线程
// Worker线程的全局对象为WorkerGlobalScrip,通过self或this援用。调用全局对象的属性和办法时能够省略全局对象。                                                                                                                                                                                                                                                                                                                                                                    // 接管主线程向worker线程发送的音讯                                                                                                                                                                                               self.addEventListener('message', event => {                                                                                                                                                                                         console.log(event.data)                                                                                                                                                                                                       })                                                                                                                                                                                                                                addEventListener('message', event => {                                                                                                                                                                                              console.log(event.data)                                                                                                                                                                                                       })                                                                                                                                                                                                                                this.onmessage = event => {                                                                                                                                                                                                         console.log(event.data)                                                                                                                                                                                                       }                                                                                                                                                                                                                                 // 当发送的音讯序列化失败时触发该事件。                                                                                                                                                                                           self.onmessageerror = error => console.error(error)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     // 向主线程发送音讯                                                                                                                                                                                                               self.postMessage('send text to main worker')                                                                                                                                                                                                                                                                                                                                                                                                                                    // 完结本身所在的Worker线程                                                                                                                                                                                                       self.close()                                                                                                                                                                                                                                                                                                                                                                                                                                                                    // 导入其余脚本到以后的Worker线程,不要求所援用的脚本必须同域。                                                                                                                                                                   self.importScripts('script1.js', 'script2.js')                                                                                                                                                                                    

通过WebWorker运行本页脚本

形式1——BlobURL.createObjectURL

限度:UI线程所属页面不是本地页面,即必须为http(s)://协定。

const script = `addEventListener('message', event => {                                                                                                                                                                                      console.log(event.data)                                                                                                                                                                                                                 postMessage('echo')                                                                                                                                                                                                                   }`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    const blob = new Blob([script])                                                                                                                                                                                                           const url = URL.createObjectURL(blob)                                                                                                                                                                                                     const worker = new Worker(url)                                                                                                                                                                                                            worker.onmessage = event => console.log(event.data)                                                                                                                                                                                       worker.postMessage('main thread')                                                                                                                                                                                                         setTimeout(()=>{                                                                                                                                                                                                                            worker.terminate()                                                                                                                                                                                                                      URL.revokeObjectURL(url) // 必须手动开释资源,否则须要刷新Browser Context时才会被开释。                                                                                                                                               }, 1000)  

形式2——Data URL

限度:无奈利用JavaScript的ASI机制少写分号。
长处:即便UI线程所属页面是本地页面也能够执行。

// 因为Data URL的内容为必须压缩为一行,因而JavaScript无奈利用换行符达到分号的成果。                                                                                                                                                       const script = `addEventListener('message', event => {                                                                                                                                                                                      console.log(event.data);                                                                                                                                                                                                                postMessage('echo');                                                                                                                                                                                                                  }`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               const worker = new Worker(`data:,${script}`)                                                                                                                                                                                              // 或 const worker = new Worker(`data:application/javascript,${script}`)                                                                                                                                                                  worker.onmessage = event => console.log(event.data)                                                                                                                                                                                       worker.postMessage('main thread')  

Shared Web Worker详解

共享线程能够和多个同域页面间通信,当所有相干页面都敞开时共享线程才会被开释。
这里的多个同域页面包含:

  1. iframe之间
  2. 浏览器标签页之间

简略示例

  1. UI主线程
const worker = new SharedWorker('./worker.js')                                                                                                                                                                                             worker.port.addEventListener('message', e => {                                                                                                                                                                                               console.log(e.data)                                                                                                                                                                                                                      }, false)                                                                                                                                                                                                                                  worker.port.start()  // 连贯worker线程                                                                                                                                                                                                     worker.port.postMessage('hi')                                                                                                                                                                                                                                                                                                                                                                                                                                                            setTimeout(()=>{                                                                                                                                                                                                                             worker.port.close() // 敞开连贯                                                                                                                                                                                                          }, 10000)                                                                                                                                                                                                                                  
  1. Shared Web Worker线程
let conns = 0                                                                                                                                                                                                                                                                                                                                                                                                                                                                            // 当UI线程执行worker.port.start()时触发建设连贯                                                                                                                                                                                           self.addEventListener('connect', e => {                                                                                                                                                                                                      const port = e.ports[0]                                                                                                                                                                                                                    conns+=1                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 port.addEventListener('message', e => {                                                                                                                                                                                                      console.log(e.data)  // 留神console对象指向第一个创立Worker线程的UI线程的console对象。即如果A先创立Worker线程,那么后续B、C等UI线程执行worker.port.postMessage时回显信念仍然会发送给A页面。                                              })                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       // 建设双向连贯,可互相通信                                                                                                                                                                                                                port.start()                                                                                                                                                                                                                               port.postMessage('hey')                                                                                                                                                                                                                  })                                                                                                                                                                                                                                         

示例——播送

  1. UI主线程
   const worker = new SharedWorker('./worker.js')                                                                                                                                                                                                worker.port.addEventListener('message', e => {                                                                                                                                                                                                  console.log('SUM:', e.data)                                                                                                                                                                                                                 }, false)                                                                                                                                                                                                                                     worker.port.start()  // 连贯worker线程                                                                                                                                                                                                                                                                                                                                                                                                                                                      const button = document.createElement('button')                                                                                                                                                                                               button.textContent = 'Increment'                                                                                                                                                                                                              button.onclick = () => worker.port.postMessage(1)                                                                                                                                                                                             document.body.appendChild(button)                                                                                                                                                                                                          
  1. Shared Web Worker线程
   let sum = 0                                                                                                                                                                                                                                   const conns = []                                                                                                                                                                                                                              self.addEventListener('connect', e => {                                                                                                                                                                                                         const port = e.ports[0]                                                                                                                                                                                                                       conns.push(port)                                                                                                                                                                                                                                                                                                                                                                                                                                                                            port.addEventListener('message', e => {                                                                                                                                                                                                         sum += e.data                                                                                                                                                                                                                                 conns.forEach(conn => conn.postMessage(sum))                                                                                                                                                                                                })                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          port.start()                                                                                                                                                                                                                                })                                                                                                                                                                                                                                         

即便是Web Worker也阻止不了你卡死浏览器的信心

通过WebWorker执行计算密集型工作是否就能够胡作非为地编写代码,并保障用户界面操作晦涩呢?当然不是啦,工具永远只能让你更好地实现工作,但无奈禁止你用错。
只有在频繁继续执行的代码中退出console对象办法的调用,加上一不小心关上Devtools工具,卡死浏览器几乎不能再就简略了。这是为什么呢?
因为UI线程在创立WebWorker线程时会将本身的console对象绑定给WebWorker线程的console属性上,那么WebWorker线程是以同步阻塞形式调用console将参数传递给UI线程的console对象,天然会占用UI线程的解决工夫。

工程化——通过Webpack的worker-loader打包代码

下面说了这么多那理论我的项目中应该怎么应用呢?或者说如何更好的集成到工程自动化工具——Webpack呢?
worker-loader和shared-worker-loader就是咱们想要的。
通过worker-loader将代码转换为Blob类型,并通过URL.createObjectURL创立url调配给WebWorker线程执行。

  1. 装置loader
npm install worker-loader -D
  1. 配置Webpack.config.js
// 解决worker代码的loader必须位于js和ts之前                                                                                                                                                                                              {                                                                                                                                                                                                                                         test: /\.worker\.ts$/,                                                                                                                                                                                                                  use: {                                                                                                                                                                                                                                  loader: 'worker-loader',                                                                                                                                                                                                              options: {                                                                                                                                                                                                                              name: '[name]:[hash:8].js', // 打包后的chunk的名称                                                                                                                                                                                    inline: true // 开启内联模式,将chunk的内容转换为Blob对象内嵌到代码中。                                                                                                                                                               }                                                                                                                                                                                                                                     }                                                                                                                                                                                                                                      },                                                                                                                                                                                                                                      {                                                                                                                                                                                                                                         test: /\.js$/,                                                                                                                                                                                                                          use: {                                                                                                                                                                                                                                   loader: 'babel-loader'                                                                                                                                                                                                                },                                                                                                                                                                                                                                      exclude: [path.resolve(__dirname, 'node_modules')]                                                                                                                                                                                     },                                                                                                                                                                                                                                       {                                                                                                                                                                                                                                         test: /\.ts(x?)$/,                                                                                                                                                                                                                      use: [                                                                                                                                                                                                                                    { loader: 'babel-loader' },                                                                                                                                                                                                             { loader: 'ts-loader' } // loader程序从后往前执行                                                                                                                                                                                     ],                                                                                                                                                                                                                                      exclude: [path.resolve(__dirname, 'node_modules')]                                                                                                                                                                                     }                                                                                                                                                                                                                                                
  1. UI线程代码
import MyWorker from './my.worker'                                                                                                                                                                                                                                                                                                                                                                                                                                                     const worker = new MyWorker('');                                                                                                                                                                                                         worker.postMessage('hi')                                                                                                                                                                                                                 worker.addEventListener('message', event => console.log(event.data))   
  1. Worker线程代码
cosnt worker: Worker = self as any                                                                                                                                                                                                       worker.addEventListener('message', event => console.log(event.data))                                                                                                                                                                                                                                                                                                                                                                                                                   export default null as any // 标识以后为TS模块,防止报xxx.ts is not a module的异样   

工程化——RPC类库Comlink

个别场景下咱们会这样应用WebWorker,

  1. UI线程传递参数并调用运算函数;
  2. 在不影响用户界面响应的前提下期待函数返回值;
  3. 获取函数返回值持续后续代码。

翻译为代码就是

let arg1 = getArg1()let arg2 = getArg2()const result = await performCalcuation(arg1, arg2)doSomething(result)

而UI线程和WebWorker线程的音讯机制通信机制显然会加大代码复杂度,而Comlink类库恰好能抚平这道伤疤。

  1. UI线程
import * as Comlink from 'comlink'                                                                                                                                                                                                                                                                                                                                                                                                                                                     async function init() {                                                                                                                                                                                                                   const cl = Comlink.wrap(new Worker('worker.js'))                                                                                                                                                                                      console.log(`Counter: ${await cl.counter}`)                                                                                                                                                                                           await cl.inc()                                                                                                                                                                                                                        console.log(`Counter: ${await cl.counter}`)                                                                                                                                                                                          }                                                                                                                                                                                                                                        
  1. Worker线程
import * as Comlink from 'comlink'                                                                                                                                                                                                                                                                                                                                                                                                                                                     const obj = {                                                                                                                                                                                                                              counter: 0,                                                                                                                                                                                                                              inc() {                                                                                                                                                                                                                                    this.counter+=1                                                                                                                                                                                                                        }                                                                                                                                                                                                                                      }                                                                                                                                                                                                                                        Comlink.expose(obj)                                                                                                                                                                                                                      

Electron中应用WebWorker

Electron中应用Web Worker的同源限度中开了个口——UI线程所属页面URL为本地文件时,所调配给Web Worker的脚本可为本地脚本。
其实Electron打包后读取的HTML页面、脚本等都是本地文件,如果不能调配本地脚本给Web Worker执行,那就进入死胡同了。

const path = window.require('path')                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               const worker = new Worker(path.resolve(__dirname, 'worker.js'))                                                                                                                                                                                                         

上述代码仅示意Electron能够调配本地脚本给WebWorker线程执行,但理论开发阶段个别是通过 ~http(s)://~ 协定加载页面资源,而公布时才会打包为本地资源。
所以这里还要分为开发阶段用和公布用代码,还波及资源的门路问题,所以还不如间接转换为Blob数据内嵌到UI线程的代码中更便捷。

总结

随着边缘计算的衰亡,客户端承当局部计算工作进步运算时效性和升高服务端压力必将成为趋势。WebWorker这一秘技你Get到了吗?:)
转载请注明来自: https://www.cnblogs.com/fsjoh... —— 肥仔John