嵌入式 Web Workers 本质上就是把代码当作字符串处理;如果是字符串我们可存放的地方就太多了,可以放在 JavaScript 的变量中、利用函数的 toString 方法能够输出本函数所有代码的字符串的特性、放在 type 没有被指定可运行的 mime-type 的 <script> 标签中等等。
但是,我们会发现一个问题,字符串怎么当作一个地址传入 Worker 的构造器呢?有什么 API 能够生成 URL 呢?URL.createObjectURL 方法可以,可是这个 API 能够接收字符串吗?查阅文档,我们知道这个方法接收一个 Blob 对象,这个对象实例在创建时,第一个参数允许接收字符串,第二个参数接收一个配置对象,其中的 type 属性能够指定生成的对象实例的类型。现在,我们已经知道了嵌入式 Web Workers 的工作原理,接下来,我们通过 Demo 来看下代码:
转移消息(Transferring the message):这种方式传递的是 可转让对象,可转让对象从一个上下文转移到另一个上下文并不会经过任何拷贝操作;因此,一旦对象转让,那么它在原来上下文的那个版本将不复存在,该对象的所有权被转让到新的上下文内;这意味着消息发送者一旦发送消息,就再也无法使用发出的消息数据了。这样的消息传递几乎是瞬时的,在传递大数据时会获得极大的性能提升。
博客 有更多精品文章哟。
修订
importScripts
跨域时,使用相对路径报错的原因说明。前言
JavaScript 采用的是单线程模型,也就是说,所有任务都要在一个线程上完成,一次只能执行一个任务。有时,我们需要处理大量的计算逻辑,这是比较耗费时间的,用户界面很有可能会出现假死状态,非常影响用户体验。这时,我们就可以使用 Web Workers 来处理这些计算。
Web Workers 是 HTML5 中定义的规范,它允许 JavaScript 脚本运行在主线程之外的后台线程中。这就为 JavaScript 创造了 多线程 的环境,在主线程,我们可以创建 Worker 线程,并将一些任务分配给它。Worker 线程与主线程同时运行,两者互不干扰。等到 Worker 线程完成任务,就把结果发送给主线程。
Web Workers 的优点是显而易见的,它可以使主线程能够腾出手来,更好的响应用户的交互操作,而不必被一些计算密集或者高延迟的任务所阻塞。但是,Worker 线程也是比较耗费资源的,因为它一旦创建,就一直运行,不会被用户的操作所中断;所以当任务执行完毕,Worker 线程就应该关闭。
Web Workers API
一个 Worker 线程是由
new
命令调用Worker()
构造函数创建的;构造函数的参数是:包含执行任务代码的脚本文件,引入脚本文件的 URI 必须遵守 同源策略。Worker 线程与主线程不在同一个全局上下文中,因此会有一些需要注意的地方:
window
和parent
这些对象,但是可以使用与主线程全局上下文无关的东西,例如WebScoket
、indexedDB
和navigator
这些对象,更多能够使用的对象可以查看Web Workers可以使用的函数和类。工作流程
WorkerRunLoop
的消息队列中;此时,如果 Worker 线程还未创建,那么消息会先存放在临时消息队列,等待 Worker 线程创建后再转移到WorkerRunLoop
的消息队列中;否则,直接将消息添加到WorkerRunLoop
的消息队列中。Worker 线程向主线程发送的消息也会通过 中转对象 进行传递;因此,总得来讲 Worker 的工作机制就是通过 中转对象 来实现消息的传递,再通过
message
事件来完成消息的处理。使用方式
Web Workers 规范中定义了两种不同类型的线程:
专用线程
下面代码最重要的部分在于两个线程之间怎么发送和接收消息,它们都是使用
postMessage
方法发送消息,使用onmessage
事件进行监听。区别是:在主线程中,onmessage
事件和postMessage
方法必须挂载在 Worker 的实例上;而在 Worker 线程,Worker 的实例方法本身就是挂载在全局上下文上的。Demo
共享线程
共享线程虽然可以在多个页面共享,但是必须遵守同源策略,也就是说只能在相同协议、主机和端口号的网页使用。
示例基本上与专用线程的类似,区别是:
onmessage
事件或者显式调用start
方法打开端口连接。而在专用线程中这一部分是自动执行的。Demo
终止 Worker
如果不需要 Worker 继续运行,我们可以在主线程中调用 Worker 实例的
terminate
方法或者使用 Worker 线程的close
方法来终止 Worker 线程。Demo
处理错误
当 Worker 线程在运行过程中发生错误时,我们在主线程通过 Worker 实例的
error
事件可以接收到 Worker 线程抛出的错误;error
事件的回调函数会返回ErrorEvent
对象,我们主要关心它的三个属性:filename
,发生错误的脚本文件名。lineno
,发生错误时所在脚本文件的行号。message
,可读性良好的错误消息。Demo
生成 Sub Worker
Worker 线程本身也能创建 Worker,这样的 Worker 线程被称为 Sub Worker,它们必须与当前页面同源。另外,在创建 Sub Worker 时传入的地址是相对与当前 Worker 线程而不是页面地址,因为这样有助于记录依赖关系。
Demo
引入脚本
Worker 线程中提供了
importScripts
函数来引入脚本,该函数接收零个或者多个 URI;需要注意的是,无论引入的资源是何种类型的文件,importScripts
都会将这个文件的内容当作JavaScript
进行解析。importScripts
的加载过程和<script>
标签类似,因此使用这个函数引入脚本并 不存在跨域问题。在脚本下载时,它们的下载顺序并不固定;但是,在执行时,脚本还是会按照书写的顺序执行;并且,这一系列过程都是 同步 进行的。加载成功后,每个脚本中的全局上下文都能够在 Worker 线程中使用;另外,如果脚本无法加载,将会抛出错误,并且之后的代码也无法执行了。Demo
嵌入式 Web Workers
嵌入式 Web Workers 本质上就是把代码当作字符串处理;如果是字符串我们可存放的地方就太多了,可以放在
JavaScript
的变量中、利用函数的toString
方法能够输出本函数所有代码的字符串的特性、放在type
没有被指定可运行的mime-type
的<script>
标签中等等。但是,我们会发现一个问题,字符串怎么当作一个地址传入 Worker 的构造器呢?有什么 API 能够生成 URL 呢?
URL.createObjectURL
方法可以,可是这个 API 能够接收字符串吗?查阅文档,我们知道这个方法接收一个Blob
对象,这个对象实例在创建时,第一个参数允许接收字符串,第二个参数接收一个配置对象,其中的type
属性能够指定生成的对象实例的类型。现在,我们已经知道了嵌入式 Web Workers 的工作原理,接下来,我们通过 Demo 来看下代码:数据通讯
Worker 线程和主线程进行通信,除了使用上面例子中 Worker 实例的
postMessage
方法之外,还可以使用 Broadcast Channel(广播通道)。Broadcast Channel(广播通道)
Broadcast Channel 允许我们在同源的所有上下文中发送和接收消息,包括浏览器标签页、iframe 和 Web Workers。需要注意的是这个 API 的兼容性并不好,在 caniuse 中我们可以查看浏览器的支持情况。另外,下图能帮助我们更好的理解 Broadcast Channel 的通信过程:
这个 API 的使用方法与 Web Workers 类似,发送和接收也是通过实例的
postMessage
方法和message
事件;不同在于构造器是BroadcastChannel
,并且它会接收一个频道名称字符串;有着相同频道名称的Broadcast Channel
实例在同一个广播通道中,因此,它们可以相互通信。Demo
消息机制
在 Web Workers 中根据不同的消息格式,有两种发送消息的方式:
JSON.stringify
方法把JSON
数据转换成字符串,再通过JSON.parse
方法进行解析是一样的过程,只不过浏览器自动帮我们做了这些工作。经过编码/解码的过程后,我们知道主线程和 Worker 线程并不会共用一个消息实例,它们每次通信都会创建消息副本;这样一来,传递的 消息越大,时间开销就越多。另外,不同的浏览器实现会有所差别,并且旧版本还有兼容问题,因此比较推荐 手动 编码成 字符串 /解码成序列化数据来传递复杂格式的消息。我们通过 Demo 来观察下两者的时间差异:
10 次比较都使用了相同的数据(1024 1024 32),0 列表示拷贝消息,1 列表示转移消息;可以发现转移消息损失的时间基本可以忽略不计,而拷贝消息消耗的时间非常的大;因此,我们在传递消息时,如果数据比较小,可以直接使用拷贝消息,但是如果数据非常大,那最好使用可转让对象进行消息转移。
跨域
Worker 在实例化时必须传入同源脚本的地址,否则就会报跨域错误:
很多时候,我们都需要把脚本放在 CDN 上面,很容易出现跨域问题,有什么办法能避免跨域呢?
异步
我们看完上文后知道 嵌入式 Web Workers 的本质就是利用了字符串,那我们通过异步的方式先获取到
JavaScript
文件的内容,然后再生成同源的 URL,这样 Worker 的构造器自然就能顺利运行了;因此,这种方案主要需要解决的问题是异步跨域;异步跨域最简单的方式莫过于使用 CORS 了,我们来看下 Demo(本地的两个server*.js
都要通过node
运行)。importScripts
这种方式实际上也是 嵌入式 Web Workers,不过利用了
importScripts
引入脚本没有跨域问题这一特性;首先我们生成引入脚本的代码字符串,然后创建同源的 URL,最后运行 Worker 线程;此时,嵌入式 Web Workers 执行importScripts
引入了跨域的脚本,最终的执行效果就跟放在同源一样了。Demo
相对路径
另外,在使用这个方法跨域时,如果通过
importScripts
函数使用相对路径的脚本,会有报错,提示我们脚本没有加载成功。出现这个报错的原因在于通过
window.URL.createObjectURL
生成的blob
链接,指向的是内存中的数据,这些数据只为当前页面提供服务,因此,在浏览器的地址栏中访问blob
链接,并不会找到实际的文件;同样的,我们在blob
链接指向的内存数据中访问相对地址,肯定是找不到任何东西的。所以,如果想要在这种场景中访问文件,那我们必须向服务器发送 HTTP 请求来获取数据。
总结
到此为止,我们已经对 Worker 有了深入的了解,知道了它的作用、使用方式和限制;在真实的场景中,我们也就能够针对最适合的业务使用正确的方式进行使用和规避限制了。
最后,我们可以畅想一下 Web Workers 的使用场景:
还有好多应用场景,可以看参考资料中的文章进行了解。
参考资料