第五章: 程序性能
最后更新于:2022-04-02 02:04:12
# 第五章: 程序性能
这本书至此一直是关于如何更有效地利用异步模式。但是我们还没有直接解释为什么异步对于JS如此重要。最明显明确的理由就是 性能。
举个例子,如果你要发起两个Ajax请求,而且他们是相互独立的,但你在进行下一个任务之前需要等到他们全部完成,你就有两种选择来对这种互动建立模型:顺序和并发。
你可以发起第一个请求并等到它完成再发起第二个请求。或者,就像我们在promise和generator中看到的那样,你可以“并列地”发起两个请求,并在继续下一步之前让一个“门”等待它们全部完成。
显然,后者要比前者性能更好。而更好的性能一般都会带来更好的用户体验。
异步(并发穿插)甚至可能仅仅增强高性能的印象,即便整个程序依然要用相同的时间才成完成。用户对性能的印象意味着一切——如果不能再多的话!——和实际可测量的性能一样重要。
现在,我们想超越局部的异步模式,转而在程序级别的水平上讨论一些宏观的性能细节。
注意: 你可能会想知道关于微性能问题,比如`a++`与`++a`哪个更快。我们会在下一章“基准分析与调优”中讨论这类性能细节。
## Web Workers
如果你有一些处理密集型的任务,但你不想让它们在主线程上运行(那样会使浏览器/UI变慢),你可能会希望JavaScript可以以多线程的方式操作。
在第一章中,我们详细地谈到了关于JavaScript如何是单线程的。那仍然是成立的。但是单线程不是组织你程序运行的唯一方法。
想象将你的程序分割成两块儿,在UI主线程上运行其中的一块儿,而在一个完全分离的线程上运行另一块儿。
这样的结构会引发什么我们需要关心的问题?
其一,你会想知道运行在一个分离的线程上是否意味着它在并行运行(在多CPU/内核的系统上),如此在第二个线程上长时间运行的处理将 不会 阻塞主程序线程。否则,“虚拟线程”所带来的好处,不会比我们已经在异步并发的JS中得到的更多。
而且你会想知道这两块儿程序是否访问共享的作用域/资源。如果是,那么你就要对付多线程语言(Java,C++等等)的所有问题,比如协作式或抢占式锁定(互斥,等)。这是很多额外的工作,而且不应当轻易着手。
换一个角度,如果这两块儿程序不能共享作用域/资源,你会想知道它们将如何“通信”。
所有这些我们需要考虑的问题,指引我们探索一个在近HTML5时代被加入web平台的特性,称为“Web Worker”。这是一个浏览器(也就是宿主环境)特性,而且几乎和JS语言本身没有任何关系。也就是说,JavaScript *当前* 并没有任何特性可以支持多线程运行。
但是一个像你的浏览器那样的环境可以很容易地提供多个JavaScript引擎实例,每个都在自己的线程上,并允许你在每个线程上运行不同的程序。你的程序中分离的线程块儿中的每一个都称为一个“(Web)Worker”。这种并行机制叫做“任务并行机制”,它强调将你的程序分割成块儿来并行运行。
在你的主JS程序(或另一个Worker)中,你可以这样初始化一个Worker:
```source-js
var w1 = new Worker( "http://some.url.1/mycoolworker.js" );
```
这个URL应当指向JS文件的位置(不是一个HTML网页!),它将会被加载到一个Worker。然后浏览器会启动一个分离的线程,让这个文件在这个线程上作为独立的程序运行。
注意: 这种用这样的URL创建的Worker称为“专用(Dedicated)Wroker”。但与提供一个外部文件的URL不同的是,你也可以通过提供一个Blob URL(另一个HTML5特性)来创建一个“内联(Inline)Worker”;它实质上是一个存储在单一(二进制)值中的内联文件。但是,Blob超出了我们要在这里讨论的范围。
Worker不会相互,或者与主程序共享任何作用域或资源——那会将所有的多线程编程的噩梦带到我们面前——取而代之的是一种连接它们的基本事件消息机制。
`w1`Worker对象是一个事件监听器和触发器,它允许你监听Worker发出的事件也允许你向Worker发送事件。
这是如何监听事件(实际上,是固定的`"message"`事件):
```source-js
w1.addEventListener( "message", function(evt){
// evt.data
} );
```
而且你可以发送`"message"`事件给Worker:
```source-js
w1.postMessage( "something cool to say" );
```
在Worker内部,消息是完全对称的:
```source-js
// "mycoolworker.js"
addEventListener( "message", function(evt){
// evt.data
} );
postMessage( "a really cool reply" );
```
要注意的是,一个专用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 环境
在Worker内部,你不能访问主程序的任何资源。这意味着你不能访问它的任何全局变量,你也不能访问页面的DOM或其他资源。记住:它是一个完全分离的线程。
然而,你可以实施网络操作(Ajax,WebSocket)和设置定时器。另外,Worker可以访问它自己的几个重要全局变量/特性的拷贝,包括`navigator`,`location`,`JSON`,和`applicationCache`。
你还可以使用`importScripts(..)`加载额外的JS脚本到你的Worker中:
```source-js
// 在Worker内部
importScripts( "foo.js", "bar.js" );
```
这些脚本会被同步地加载,这意味着在文件完成加载和运行之前,`importScripts(..)`调用会阻塞Worker的执行。
注意: 还有一些关于暴露`
';