Web Worker
Web Worker 为 Web 内容在后台线程中运行脚本提供了一种简单的方法,线程可以执行任务而不干扰用户界面。
它有以下特点:
- 异步多线程 在主线程运行的同时,Worker 线程在后台运行,两者互不干扰
- 同源限制 分配给 Worker 线程运行的脚本文件,必须与主线程的脚本文件同源,Worker 也仅能被生成它的脚本所使用
- DOM 限制 Worker 线程所在的全局对象,与主线程不一样,无法读取主线程所在网页的 DOM 对象,也无法使用
document
、window
、parent
这些对象。但是,Worker 线程可以访问navigator
和location
对象,可以使用大量 window 对象之下的东西,包括 WebSockets , IndexedDB - message通信机制 Worker 线程和主线程间的数据传递通过这样的消息机制进行——双方都使用
postMessage()
方法发送各自的消息,使用onmessage
事件处理函数来响应消息 - 支持web IO Worker 线程可以使用 XMLHttpRequest 进行网络I/O
基本用法
主线程
主线程采用 new 命令,调用 Worker() 构造函数,新建一个 Worker 线程
1 | var worker = new Worker('work.js'); |
Worker() 构造函数的参数是一个脚本文件,该文件就是 Worker 线程所要执行的任务。由于 Worker 不能读取本地文件,所以这个脚本必须来自网络。
然后,主线程调用 worker.postMessage() 方法,向 Worker 发消息。
1 | worker.postMessage('Hello World'); |
worker.postMessage() 方法的参数,就是主线程传给 Worker 的数据。它可以是各种数据类型,包括二进制数据。
接着,主线程通过 worker.onmessage 指定监听函数,接收子线程发回来的消息。
1 | worker.onmessage = function (event) { |
上面代码中,事件对象的data属性可以获取 Worker 发来的数据。
Worker 完成任务以后,主线程就可以把它关掉。
1 | worker.terminate(); |
worker线程
Worker 线程内部需要有一个监听函数,监听message事件,监听函数的参数是一个事件对象,它的 data 属性包含主线程发来的数据。postMessage() 方法用来向主线程发送消息。
1 | this.onmessage(function (e) { |
Worker 内部可以通过 importScripts()
来加载其他脚本,脚本的下载顺序不固定,但执行时会按照传入 importScripts()
中的文件名顺序进行。这个过程是同步完成的;直到所有脚本都下载并运行完毕,importScripts()
才会返回。
1 | importScripts('script1.js', 'script2.js'); |
Worker 线程内部可以调用 close()
结束自身
1 | close(); |
错误处理
主线程可以监听 Worker 是否发生错误。如果发生错误,Worker 会触发主线程的 error 事件。
1 | worker.onerror(function(event) { |
数据通信
在主页面与 worker 之间传递的数据是通过拷贝,而不是共享来完成的。传递给 worker 的对象需要经过序列化,接下来在另一端还需要反序列化。页面与 worker 不会共享同一个实例,最终的结果就是在每次通信结束时生成了数据的一个副本。大部分浏览器使用结构化拷贝来实现该特性。
主线程与 Worker 之间也可以交换二进制数据,比如 File、Blob、ArrayBuffer 等类型,也可以在线程之间发送。下面是一个例子。
1 | // 主线程 |
但是,拷贝方式发送二进制数据,会造成性能问题。比如,主线程向 Worker 发送一个 500MB 文件,默认情况下浏览器会生成一个原文件的拷贝。为了解决这个问题,JavaScript 允许主线程把二进制数据直接转移给子线程,但是一旦转移,主线程就无法再使用这些二进制数据了,这是为了防止出现多个线程同时修改数据的麻烦局面。这种转移数据的方法,叫做 Transferable Objects。这使得主线程可以快速把数据交给 Worker,对于影像处理、声音处理、3D 运算等就非常方便了,不会产生性能负担。
如果要直接转移数据的控制权,就要使用下面的写法。
1 | // Transferable Objects 格式 |
同页面的 Web Worker
通常情况下,Worker 载入的是一个单独的 JavaScript 脚本文件,但是也可以载入与主线程在同一个网页的代码。
1 |
|
上面是一段嵌入网页的脚本,注意必须指定 <script>
标签的type属性是一个浏览器不认识的值,上例是 app/worker。
然后,读取这一段嵌入页面的脚本,用 Worker 来处理。
1 | var blob = new Blob([document.querySelector('#worker').textContent]); |
上面代码中,先将嵌入网页的脚本代码,转成一个二进制对象,然后为这个二进制对象生成 URL,再让 Worker 加载这个 URL。这样就做到了,主线程和 Worker 的代码都在同一个网页上面。
实例:Worker 线程完成轮询
有时,浏览器需要轮询服务器状态,以便第一时间得知状态改变。这个工作可以放在 Worker 里面。
1 | function createWorker(f) { |
上面代码中,Worker 每秒钟轮询一次数据,然后跟缓存做比较。如果不一致,就说明服务端有了新的变化,因此就要通知主线程。