在第一章中,我们详细地谈到了关于JavaScript如何是单线程的。那仍然是成立的。但是单线程不是组织你程序运行的唯一方法。

    想象将你的程序分割成两块儿,在UI主线程上运行其中的一块儿,而在一个完全分离的线程上运行另一块儿。

    这样的结构会引发什么我们需要关心的问题?

    其一,你会想知道运行在一个分离的线程上是否意味着它在并行运行(在多CPU/内核的系统上),如此在第二个线程上长时间运行的处理将 不会 阻塞主程序线程。否则,“虚拟线程”所带来的好处,不会比我们已经在异步并发的JS中得到的更多。

    而且你会想知道这两块儿程序是否访问共享的作用域/资源。如果是,那么你就要对付多线程语言(Java,C++等等)的所有问题,比如协作式或抢占式锁定(互斥,等)。这是很多额外的工作,而且不应当轻易着手。

    换一个角度,如果这两块儿程序不能共享作用域/资源,你会想知道它们将如何“通信”。

    所有这些我们需要考虑的问题,指引我们探索一个在近HTML5时代被加入web平台的特性,称为“Web Worker”。这是一个浏览器(也就是宿主环境)特性,而且几乎和JS语言本身没有任何关系。也就是说,JavaScript 当前 并没有任何特性可以支持多线程运行。

    但是一个像你的浏览器那样的环境可以很容易地提供多个JavaScript引擎实例,每个都在自己的线程上,并允许你在每个线程上运行不同的程序。你的程序中分离的线程块儿中的每一个都称为一个“(Web)Worker”。这种并行机制叫做“任务并行机制”,它强调将你的程序分割成块儿来并行运行。

    在你的主JS程序(或另一个Worker)中,你可以这样初始化一个Worker:

    这个URL应当指向JS文件的位置(不是一个HTML网页!),它将会被加载到一个Worker。然后浏览器会启动一个分离的线程,让这个文件在这个线程上作为独立的程序运行。

    注意: 这种用这样的URL创建的Worker称为“专用(Dedicated)Wroker”。但与提供一个外部文件的URL不同的是,你也可以通过提供一个Blob URL(另一个HTML5特性)来创建一个“内联(Inline)Worker”;它实质上是一个存储在单一(二进制)值中的内联文件。但是,Blob超出了我们要在这里讨论的范围。

    Worker不会相互,或者与主程序共享任何作用域或资源——那会将所有的多线程编程的噩梦带到我们面前——取而代之的是一种连接它们的基本事件消息机制。

    Worker对象是一个事件监听器和触发器,它允许你监听Worker发出的事件也允许你向Worker发送事件。

    这是如何监听事件(实际上,是固定的"message"事件):

    1. w1.addEventListener( "message", function(evt){
    2. // evt.data
    3. } );

    而且你可以发送"message"事件给Worker:

      要注意的是,一个专用Worker与它创建的程序是一对一的关系。也就是,"message"事件不需要消除任何歧义,因为我们可以确定它只可能来自于这种一对一关系——不是从Wroker来的,就是从主页面来的。

      通常主页面的程序会创建Worker,但是一个Worker可以根据需要初始化它自己的子Worker——称为subworker。有时将这样的细节委托给一个“主”Worker十分有用,它可以生成其他Worker来处理任务的一部分。不幸的是,在本书写作的时候,Chrome还没有支持subworker,然而Firefox支持。

      要从创建一个Worker的程序中立即杀死它,可以在Worker对象(就像前一个代码段中的w1)上调用terminate()。突然终结一个Worker线程不会给它任何机会结束它的工作,或清理任何资源。这和你关闭浏览器的标签页来杀死一个页面相似。

      如果你在浏览器中有两个或多个页面(或者打开同一个页面的多个标签页!),试着从同一个文件URL中创建Worker,实际上最终结果是完全分离的Worker。待一会儿我们就会讨论“共享”Worker的方法。

      注意: 看起来一个恶意的或者是呆头呆脑的JS程序可以很容易地通过在系统上生成数百个Worker来发起拒绝服务攻击(Dos攻击),看起来每个Worker都在自己的线程上。虽然一个Worker将会在存在于一个分离的线程上是有某种保证的,但这种保证不是没有限制的。系统可以自由决定有多少实际的线程/CPU/内核要去创建。没有办法预测或保证你能访问多少,虽然很多人假定它至少和可用的CPU/内核数一样多。我认为最安全的臆测是,除了主UI线程外至少有一个线程,仅此而已。

      在Worker内部,你不能访问主程序的任何资源。这意味着你不能访问它的任何全局变量,你也不能访问页面的DOM或其他资源。记住:它是一个完全分离的线程。

      然而,你可以实施网络操作(Ajax,WebSocket)和设置定时器。另外,Worker可以访问它自己的几个重要全局变量/特性的拷贝,包括navigatorlocationJSON,和。

      你还可以使用importScripts(..)加载额外的JS脚本到你的Worker中:

      1. // 在Worker内部
      2. importScripts( "foo.js", "bar.js" );

      这些脚本会被同步地加载,这意味着在文件完成加载和运行之前,importScripts(..)调用会阻塞Worker的执行。

      注意: 还有一些关于暴露<canvas>API给Worker的讨论,其中包括使canvas成为Transferable的(见“数据传送”一节),这将允许Worker来实施一些精细的脱线程图形处理,在高性能的游戏(WebGL)和其他类似应用中可能很有用。虽然这在任何浏览器中都还不存在,但是很有可能在近未来发生。

      Web Worker的常见用途是什么?

      • 处理密集型的数学计算
      • 大数据集合的排序
      • 数据操作(压缩,音频分析,图像像素操作等等)
      • 高流量网络通信

      你可能注意到了这些用途中的大多数的一个共同性质,就是它们要求使用事件机制穿越线程间的壁垒来传递大量的信息,也许是双向的。

      在Worker的早期,将所有数据序列化为字符串是唯一的选择。除了在两个方向上进行序列化时速度上变慢了,另外一个主要缺点是,数据是被拷贝的,这意味着内存用量翻了一倍(以及在后续垃圾回收上的流失)。

      谢天谢地,现在我们有了几个更好的选择。

      如果你传递一个对象,在另一端一个所谓的“结构化克隆算法(Structured Cloning Algorithm)”(

      选择使用Transferable对象不需要你做太多;任何实现了Transferable接口(https://developer.mozilla.org/en-US/docs/Web/API/Transferable)的数据结构都将自动地以这种方式传递(Firefox和Chrome支持此特性)。

      举个例子,有类型的数组如Uint8Array(见本系列的 ES6与未来)是一个“Transferables”。这是你如何用postMessage(..)来传送一个Transferable对象:

      1. // `foo` 是一个 `Uint8Array`
      2. postMessage( foo.buffer, [ foo.buffer ] );

      第一个参数是未经加工的缓冲,而第二个参数是要传送的内容的列表。

      不支持Transferable对象的浏览器简单地降级到结构化克隆,这意味着性能上的降低,而不是彻底的特性失灵。

      如果你的网站或应用允许多个标签页加载同一个网页(一个常见的特性),你也许非常想通过防止复制专用Worker来降低系统资源的使用量;这方面最常见的资源限制是网络套接字链接,因为浏览器限制同时连接到一个服务器的连接数量。当然,限制从客户端来的链接数也缓和了你的服务器资源需求。

      在这种情况下,创建一个单独的中心化Worker,让你的网站或应用的所有网页实例可以 共享 它是十分有用的。

      这称为SharedWorker,你会这样创建它(仅有Firefox与Chrome支持此特性):

      因为一个共享Worker可以连接或被连接到你的网站上的多个程序实例或网页,Worker需要一个方法来知道消息来自哪个程序。这种唯一的标识称为“端口(port)”——联想网络套接字端口。所以调用端程序必须使用Worker的port对象来通信:

      1. // ..
      2. w1.port.postMessage( "something cool" );

      另外,端口连接必须被初始化,就像这样:

      1. w1.port.start();

      在共享Worker内部,一个额外的事件必须被处理:"connect"。这个事件为这个特定的连接提供端口object。保持多个分离的连接最简单的方法是在port上使用闭包,就像下面展示的那样,同时在"connect"事件的处理器内部定义这个连接的事件监听与传送:

      除了这点不同,共享与专用Worker的功能和语义是一样的。

      注意: 如果在一个端口的连接终结时还有其他端口的连接存活着的话,共享Worker也会存活下来,而专用Worker会在与初始化它的程序间接终结时终结。

      对于并行运行的JS程序在性能考量上,Web Worker十分吸引人。然而,你的代码可能运行在对此缺乏支持的老版本浏览器上。因为Worker是一个API而不是语法,所以在某种程度上它们可以被填补。

      如果浏览器不支持Worker,那就根本没有办法从性能的角度来模拟多线程。Iframe通常被认为可以提供并行环境,但在所有的现代浏览器中它们实际上和主页运行在同一个线程上,所以用它们来模拟并行机制是不够的。

      正如我们在第一章中详细讨论的,JS的异步能力(不是并行机制)来自于事件轮询队列,所以你可以用计时器(setTimeout(..)等等)来强制模拟的Worker是异步的。然后你只需要提供Worker API的填补就行了。这里有一份列表(

      注意: 你不能模拟同步阻塞,所以这个填补不允许使用importScripts(..)。另一个选择可能是转换并传递Worker的代码(一旦Ajax加载后),来重写一个importScripts(..)填补的一些异步形式,也许使用一个promise相关的接口。