[Effective JavaScript 笔记]第65条:不要在计算时阻塞事件队列
第61条解释了异步API怎样帮助我们防止一段程序阻塞应用程序的事件队列。使用下面代码,可以很容易使一个应用程序陷入泥潭。
while(true){}
而且它并不需要一个无限循环来写一个缓慢的程序。代码需要时间来运行,而低效的算法或数据结构可能导致运行长时间的计算。
效率不是js唯一关注的。基于事件的编程的确强加了一些特殊的约束。为了保持客户端应用程序的高度交互性和确保所有传入的请求在服务器应用程序中得到充分的服务,保持事件循环的每个轮次尽可能短是至关重要。否则,事件队列会滞销,其增长速度会超过分发处理事件处理程序的速度。在浏览器环境中,一些代价高昂的计算也会导致糟糕的用户体验,因为一个页面的用户界面无响应多数是由于在运行js代码。
那么,如果你的应用程序需要执行代价高昂的计算你该怎么办呢?没有一个完全正确的答案,但有一些通用的技术可用。也许最简单的方法是使用像Web客户端平台的Worker API这样的并发机制。这对于需要搜索大量可移动距离的人工智能游戏是一个很好的方法。游戏可能以生成大量的专门计算移动距离的worker开始。
var ai=new Worker('ai.js');
这将使用ai.js源文件作为worker的脚本,产生一个新的线程独立的事件队列的并发执行线程。该worker运行在一个完全隔离的状态--没有任何应用程序对象的直接访问。但是,应用程序与worker之间可以通过发送形式为字符串的messages来交互。所以,每当游戏需要程序计算移动时,它会发送一个消息给worker。
var userMove=/* ... */;
ai.postMessage(JSON.stringify({userMove:userMove}));
postMessage的参数被作为一个消息增加到worker的事件队列中。为了处理worker的响应,游戏会注册一个事件处理程序。
ai.onmessage=function(event){
executeMove(JSON.parse(event.data).computerMove);
};
与此同时,源文件ai.js指示worker监听消息并执行计算下一步移动所需的工作。
self.onmessage=function(event){
var userMove=JSON.parse(event.data).userMove;
var computerMove=computeNextMove(userMove);
var message=JSON.stringify({
computerMove:computerMove
});
selft.postMessage(message);
};
function computeNextMove(userMove){
//...
}
不是所有的js平台都提供类似Worker这样的API。而且有时传递消息的开销可能会过于昂贵。另一种方法是将算法分解为多个步骤,每个步骤组成一个可管理的工作块。第48条中搜索社交网络图的工作表算法。
Member.prototype.inNetwork=function(other){
var visited={};
var worklist=[this];
while(worklist.length>0){
var member=worklist.pop();
if(member === other){
return true;
}
}
return false;
};
如果这段程序核心的while循环代价太过高昂,搜索工作很可能会以不可接受的时间运行而阻塞应用程序事件队列。即使我们可以使用Worker API,它也是昂贵或不方便实现的,因为它需要复制整个网络图的状态或在worker中存储网络图的状态,并总是使用消息传递来更新和查询网络。
幸运的是,这种算法被定义为一个步骤集的序列--while循环的迭代。可以通过增加一个回调参数将inNetwork转换为一个匿名函数,并像第64条讲述的,将while循环替换一个匿名的递归函数。
Member.prototype.inNetwork=function(other,callback){
var visited={};
var worklist=[this];
function next(){
if(worklist.length === 0){
callback(false);
return;
}
var member=worklist.pop();
if(member === other){
callback(true);
return;
}
setTimeout(next,0);
}
setTimeout(next,0);
};
这段代码的工作方式,为了替换while循环,这里写了一个局部的next函数,该函数执行循环中的单个迭代然后高度应用程序事件队列来异步运行下一次迭代。这使得在些期间已经发生的其他事件被处理后才继续下一次迭代。当搜索完成后,通过迭代的next来返回,从而有效地完成循环。
要调度迭代,我们使用多数js平台都可用的、通用的setTimeout API来注册next函数,使next函数经过一段最少时间(0毫秒)后运行。这具有几乎立刻将回调函数添加到事件队列上的作用。值得注意的是,虽然setTimeout有相对稳定的跨平台移植性,但通常还有更好的替代方案。例如,在浏览器环境中,最低的超时时间被压制为4毫秒,可以采用一种替代方案,使用postMessage立即压入一个事件。
如果应用程序事件队列的每个轮次中只执行算法的一个迭代。可以调整算法,自定义每个轮次中的迭代次数。这很容易实现,只须在next函数的主要部分的外围使用一个循环计数器。
Member.prototype.inNetwork=function(other,callback){
function next(){
for(var i=0;i<10;i++){
//...
}
setTimeout(next,0);
}
setTimeout(next,0);
};
提示
-
避免在主事件队列中执行代价高昂的算法
-
在支持Worker API的平台,该API可以用来在一个独立的事件队列中运行长计算程序
-
在Worker API不可用或代价昂贵的环境中,考虑将计算程序分解到事件循环的多个轮次中
附录一:Worker
Worker是可以在后台运行的任务,它能够被轻松创建,还能向它的创建者发送消息。只要调用worker()构造函数,指定一个需要运行在worker线程内的脚本,就能创建一个worker。
注意:worker能够产生新的worker,前提是这些worker托管于相同的源内来作为它们的父页面。
Worker线程能够在不干扰UI的情况下执行任务。另外,它能够使用XMLHttpRequest来执行I/O操作,只不过XMLHttpRequest上的responseXML与channel两个属性值始终返回null。
线程安全
Worker接口会生成真正的操作系统级别的线程,如果你不小心,那么并发会对你的代码产生影响。对于web worker来说,与其他线程的通信点会被很小心的控制,这意味着你很难引发并发问题。你没有办法去访问非线程安全的组件或者是DOM,此外还需要序列化对象来与线程交互特定的数据。
worker语法
构造函数
Worker(in DOMString scriptURL);
参数
scriptURL
worker将要执行的脚本的URL。它必须遵守同源策略。
返回值
一个新的Worker对象。
方法
void postMessage(Object message[,sequence<Transferable> transferList]);
参数
message
传输给woker的对象;它将包含于传递给onmessage处理函数的事件对象中的data字段内。可以传递任意值或是经过结构拷贝算法处理过的js对象,即可以包含循环引用。
transferList
一个可选的对象数组,用于转让它们的所有权。如果一个对象的所有权被转让,那么它在原来的上下文内将不可使用,而只能在转让到的worker内可用
void terminate();
立即终止worker。该方法不会给worker留下任何完成操作的机会;就是简单的立即停止。
属性
onmessage EeventListner
一个事件监听函数,每当拥有message属性的MessageEvent从worker中冒泡出来时就会执行该函数。事件的data属性存有消息内容。
onerror EeventListner
一个事件监听函数,每当类型为error的ErrorEvent从worker中冒泡出来时就会执行该函数。
错误信息对象
-
message 一个可读性良好的错误信息
-
filename 产生错误的脚本文件名
-
lineno 发生错误时所在的脚本文件号
翻译的文章,版权归原作者所有,只用于交流与学习的目的。
原创文章,版权归作者所有,非商业转载请注明出处,并保留原文的完整链接。