JavaScript 性能利器 —— Web Worker

本文创做于 2018-12-12,2019-12-20 迁移至此

简介

Web Worker (工做线程) 是 HTML5 中提出的概念,分为两种类型,专用线程(Dedicated Web Worker) 和共享线程(Shared Web Worker)。专用线程仅能被建立它的脚本所使用(一个专用线程对应一个主线程),而共享线程可以在不一样的脚本中使用(一个共享线程对应多个主线程)。javascript

专用线程能够看作是默认状况的 Web Worker,其加上修饰词的目的是为了与共享线程进行区分。本文会较为严格地区分二者,可能较为累赘,但我的认为这是必要的。若是单纯以 Web Worker 字样出现的地方指的是二者都会有的状况。html

用途

Web Worker 的意义在于能够将一些耗时的数据处理操做从主线程中剥离,使主线程更加专一于页面渲染和交互。html5

  • 懒加载
  • 文本分析
  • 流媒体数据处理
  • canvas 图形绘制
  • 图像处理
  • ...

须要注意的点

  • 有同源限制
  • 没法访问 DOM 节点
  • 运行在另外一个上下文中,没法使用Window对象
  • Web Worker 的运行不会影响主线程,但与主线程交互时仍受到主线程单线程的瓶颈制约。换言之,若是 Worker 线程频繁与主线程进行交互,主线程因为须要处理交互,仍有可能使页面发生阻塞
  • 共享线程能够被多个浏览上下文(Browsing context)调用,但全部这些浏览上下文必须同源(相同的协议,主机和端口号)

浏览器支持度

根据 CanI Use 网站的统计,目前约有 93.05% 的浏览器支持专用线程。
16726223java

而对于共享线程,则仅有大约 41.66% 的浏览器支持。
502174web

因为专用线程和共享线程的构造方法都包含在 window 对象中,咱们在使用二者以前能够对浏览器的支持性进行判断。算法

if (window.Worker) {
    // ...
}
if (window.SharedWorker) {
    // ...
}

线程建立

专用线程由 Worker()方法建立,能够接收两个参数,第一个参数是必填的脚本的位置,第二个参数是可选的配置对象,能够指定 typecredentialsname 三个属性。canvas

var worker = new Worker('worker.js')
// var worker = new Worker('worker.js', { name: 'dedicatedWorker'})

共享线程使用 Shared Worker() 方法建立,一样支持两个参数,用法与 Worker() 一致。数组

var sharedWorker = new SharedWorker('shared-worker.js')

值得注意的是,由于 Web Worker 有同源限制,因此在本地调试的时候也须要经过启动本地服务器的方式访问,使用 file:// 协议直接打开的话将会抛出异常。浏览器

数据传递

Worker 线程和主线程都经过 postMessage() 方法发送消息,经过 onmessage 事件接收消息。在这个过程当中数据并非被共享的,而是被复制的。值得注意的是 ErrorFunction 对象不能被结构化克隆算法复制,若是尝试这么作的话会致使抛出 DATA_CLONE_ERR 的异常。另外,postMessage() 一次只能发送一个对象, 若是须要发送多个参数能够将参数包装为数组或对象再进行传递。服务器

关于 postMessage() 和结构化克隆算法(The structured clone algorithm)将在本文最后进行阐述。

下面是专用线程数据传递的示例。

// 主线程
var worker = new Worker('worker.js')
worker.postMessage([10, 24])
worker.onmessage = function(e) {
    console.log(e.data)
}

// Worker 线程
onmessage = function (e) {
    if (e.data.length > 1) {
        postMessage(e.data[1] - e.data[0])
    }
}

在 Worker 线程中,selfthis 都表明子线程的全局对象。对于监听 message 事件,如下的四种写法是等同的。

// 写法 1
self.addEventListener('message', function (e) {
    // ...
})

// 写法 2
this.addEventListener('message', function (e) {
    // ...
})

// 写法 3
addEventListener('message', function (e) {
    // ...
})

// 写法 4
onmessage = function (e) {
    // ...
}

主线程经过 MessagePort 访问专用线程和共享线程。专用线程的 port 会在线程建立时自动设置,而且不会暴露出来。与专用线程不一样的是,共享线程在传递消息以前,端口必须处于打开状态。MDN 上的 MessagePort 关于 start() 方法的描述是:

Starts the sending of messages queued on the port (only needed when using EventTarget.addEventListener; it is implied when using MessagePort.onmessage.)

这句话通过试验,能够理解为 start() 方法是与 addEventListener 配套使用的。若是咱们选择 onmessage 进行事件监听,那么将隐含调用 start() 方法。

// 主线程
var sharedWorker = new SharedWorker('shared-worker.js')
sharedWorker.port.onmessage = function(e) {
    // 业务逻辑
}
var sharedWorker = new SharedWorker('shared-worker.js')
sharedWorker.port.addEventListener('message', function(e) {
    // 业务逻辑
}, false)
sharedWorker.port.start() // 须要显式打开

在传递消息时,postMessage() 方法和 onmessage 事件必须经过端口对象调用。另外,在 Worker 线程中,须要使用 onconnect 事件监听端口的变化,并使用端口的消息处理函数进行响应。

// 主线程
sharedWorker.port.postMessage([10, 24])
sharedWorker.port.onmessage = function (e) {
    console.log(e.data)
}

// Worker 线程
onconnect = function (e) {
    let port = e.ports[0]

    port.onmessage = function (e) {
        if (e.data.length > 1) {
            port.postMessage(e.data[1] - e.data[0])
        }
    }
}

关闭 Worker

能够在主线程中使用 terminate() 方法或在 Worker 线程中使用 close() 方法关闭 worker。这两种方法是等效的,但比较推荐的用法是使用 close(),防止意外关闭正在运行的 Worker 线程。Worker 线程一旦关闭 Worker 后 Worker 将再也不响应。

// 主线程
worker.terminate()

// Dedicated Worker 线程中
self.close()

// Shared Worker 线程中
self.port.close()

错误处理

能够经过在主线程或 Worker 线程中设置 onerroronmessageerror 的回调函数对错误进行处理。其中,onerror 在 Worker 的 error 事件触发并冒泡时执行,onmessageerror 在 Worker 收到的消息不能进行反序列化时触发(本人通过尝试没有办法触发 onmessageerror 事件,若是在 worker 线程使用 postMessage 方法传递一个 Error 或 Function 对象会由于没法序列化优先被 onerror 方法捕获,而根本不会进入反序列化的过程)。

// 主线程
worker.onerror = function () {
    // ...
}

// 主线程使用专用线程
worker.onmessageerror = function () {
    // ...
}

// 主线程使用共享线程
worker.port.onmessageerror = function () {
    // ...
}

// worker 线程
onerror = function () {

}

加载外部脚本

Web Worker 提供了 importScripts() 方法,可以将外部脚本文件加载到 Worker 中。

importScripts('script1.js')
importScripts('script2.js')

// 以上写法等价于
importScripts('script1.js', 'script2.js')

子线程

Worker 能够生成子 Worker,但有两点须要注意。

  • 子 Worker 必须与父网页同源
  • 子 Worker 中的 URI 相对于父 Worker 所在的位置进行解析

嵌入式 Worker

目前没有一类标签可使 Worker 的代码像 <script> 元素同样嵌入网页中,但咱们能够经过 Blob() 将页面中的 Worker 代码进行解析。

<script id="worker" type="javascript/worker">
// 这段代码不会被 JS 引擎直接解析,由于类型是 'javascript/worker'

// 在这里写 Worker 线程的逻辑
</script>
<script>
    var workerScript = document.querySelector('#worker').textContent
    var blob = new Blob(workerScript, {type: "text/javascript"})
    var worker = new Worker(window.URL.createObjectURL(blob))
</script>

关于 postMessage

Web Worker 中,Worker 线程和主线程之间使用结构化克隆算法(The structured clone algorithm)进行数据通讯。结构化克隆算法是一种经过递归输入对象构建克隆的算法,算法经过保存以前访问过的引用的映射,避免无限遍历循环。这一过程能够理解为,在发送方使用相似 JSON.stringfy() 的方法将参数序列化,在接收方采用相似 JSON.parse() 的方法反序列化。

可是,一次数据传输就须要同时通过序列化和反序列化,若是数据量大的话,这个过程自己也可能形成性能问题。所以, Worker 中提出了 Transferable Objects 的概念,当数据量较大时,咱们能够选择在将主线程中的数据直接移交给 Worker 线程。值得注意的是,这种转移是完全的,一旦数据成功转移,主线程将不能访问该数据。这个移交的过程仍然经过 postMessage 进行传递。

postMessage(message, transferList)

例如,传递一个 ArrayBuffer 对象

let aBuffer = new ArrayBuffer(1)
worker.postMessage({ data: aBuffer }, [aBuffer])

上下文

Worker 工做在一个 WorkerGlobalDataScope 的上下文中。每个 WorkerGlobalDataScope 对象都有不一样的 event loop。这个 event loop 没有关联浏览器上下文(browsing context),它的任务队列也只有事件(events)、回调(callbacks)和联网的活动(networking activity)。

每个 WorkerGlobalDataScope 都有一个 closing 标志,当这个标志设为 true 时,任务队列将丢弃以后试图加入任务队列的任务,队列中已经存在的任务不受影响(除非另有指定)。同时,定时器将中止工做,全部挂起(pending)的后台任务将会被删除。

Worker 中可使用的函数和类

因为 Worker 工做的上下文不一样于普通的浏览器上下文,所以不能访问 window 以及 window 相关的 API,也不能直接操做 DOM。Worker 中提供了 WorkerNavigatorWorkerLocation 接口,它们分别是 window 中 NavigatorLocation 的子集。除此以外,Worker 还提供了涉及时间、存储、网络、绘图等多个种类的接口,如下列举了其中的一部分,更多的接口能够参考 MDN 文档

时间相关

  • clearInterval()
  • clearTimeout()
  • setInterval()
  • setTimeout

Worker 相关

  • importScripts()
  • close()
  • postMessage()

存储相关

  • Cache
  • IndexedDB

网络相关

  • Fetch
  • WebSocket
  • XMLHttpRequest

相关连接

参考

扩展阅读

相关文章
相关标签/搜索