No.61、不要阻塞I/O事件队列
Tips:
JavaScript程序是构建在事件之上的。在其他一些语言中,我们可能常常会实现如下代码:
var result = downFileSync('http://xxx.com');
console.log(result);
以上代码,如果downFileSync需要5分钟,那么程序就会停下来等待5分钟。这样的函数就被称为同步函数(或阻塞函数)。如果在浏览器中实现这样的函数,那么结果就是浏览器卡住,等待下载完成后,再继续响应。那么,这将极大的影响体验。所以,在JavaScript中,一般使用如下方式:
downFileAsync('http://xxx.com', function(result){
console.log(result);
});
console.log('async');
以上代码执行中,就算下载文件要5分钟,那么程序也会立马打印出“async”,然后在下载完成的时候,打印result出来。这样才能保证执行环境能正确响应客户的操作。
JavaScript并发的一个最重要的规则是绝不要在应用程序事件队列中使用阻塞I/O的API。在浏览器中,甚至基本没有任何阻塞的API是可用的。其中XMLHttpRequest库有一个同步版本的实现,被认为是一种不好的实现,影响Web应用程序的交互性。
在现代浏览器(IE10+(含)、Chrome、FireFox)中,提供了Worker的API,该API使得产生大量的并行计算称为可能。
如何使用?
首先,编写两个文件,第一个是task.js,如下:
//task.js
console.time('t1');
var sum = 0;
for(var i = 0; i < 500000000; i++){
sum += i;
}
console.log('test');
console.timeEnd('t1');
postMessage('worker result:' + sum);
然后是index.html,用于调用worker,代码如下:
// index.html
<button onclick="alert('aa')">Test</button>
<script>
var worker = new Worker('test.js');
worker.onmessage = function(evt){
console.log(evt.data);
};
</script>
在index.html的JavaScript脚本中。使用var worker = new Worker('test.js');
来实例化一个Worker,Worker的构造为:new Worker([string] url),然后注册一个onmessage事件,用于处理test.js的通知,就是test.js中的postMessage函数。test.js中的每一次执行postMessage函数都会触发一次Worker的onmessage回调。
在静态服务器中访问index.html,可以看到输出为:
test
t1: 2348.633ms
worker result:124999999567108900
再来看看Worker的优缺点,我们可以做什么:
有那些局限性:
更多信息,请参考:
Tips:
想象一下如下需求,异步请数据库查找一个地址,并异步下载。由于是异步,我们不可能发起两个连续请求,那么js代码很可能是这样的:
db.lookupAsync('url', function(url){
downloadAsync(url, function(result){
console.log(result);
});
});
我们使用嵌套,成功解决了这个问题,但是当这样的依赖很多时,我们的代码可能是这样:
db.lookupAsync('url', function(url){
downloadAsync('1.txt', function(){
downloadAsync('2.txt', function(){
downloadAsync('3.txt', function(){
//do something...
});
});
});
});
这样就陷入了回调地狱。要减少过多的嵌套的方法之一就是将回调函数作为命名的函数,并将它们需要的附加数据作为额外的参数传递。比如:
db.lookupAsync('url', downloadUrl);
function downloadUrl(url){
downloadAsync(url, printResult);
}
function printResult(result){
console.log(result);
}
这样能控制嵌套回调的规模,但是还是不够直观。实际上,在node中解决此类问题是用现有的模块,如async。
Tips:
一般情况下,我们的错误处理代码如下:
try{
a();
b();
c();
}catch(ex){
//处理错误
}
对于异步的代码,不可能将错误包装在一个try中,事实上,异步的API甚至根本不可能抛出异常。异步的API倾向于将错误表示为回调函数的特定参数,或使用一个附加的错误处理回调函数(有时被称为errbacks)。代码如下:
downloadAsync(url, function(result){
console.log(result);
}, function(err){ //提供一个单独的错误处理函数
console.log('Error:' + err);
});
多次嵌套时,错误处理函数会被多次复制,所以可以将错误处理函数提取出来,减少重复代码,代码如下:
downloadAsync('1.txt', function(result){
downloadAsync('2.txt', function(result2){
console.log(result + result2);
}, onError);
}, onError);
在node中,异步API的回调函数第一个参数表示err,这已经成为一个大众标准
Tips:
针对异步下载文件,如果要使用循环,大概是如下代码:
function downloadFilesSync(urls){
for(var i = 0, len = urls.length; i < len; i++){
try{
return downloadSync(urls[i]);
}catch(ex){
}
}
}
以上代码并不能正确工作,因为方法一调用,就会启动所有的下载,并不能等待一个完成,再继续下一个。
要实现功能,看看下面的递归代码:
function downloadFilesSync(urls){
var len = urls.length;
function tryNextURL(i) {
if (i >= n) {
console.log('Error');
return; //退出
}
downloadAsync(urls[i], function(result){
console.log(result);
//下载成功后,尝试下一个。
tryNextURL(i + 1);
});
}
tryNextURL(0);// 启动递归
}
类似这样的实现,就能解决批量下载的问题了。
Tips:
打开浏览器控制台,执行 while(true){}
,会是什么效果?
好吧,浏览器卡死了!!!
如果有这样的需求,那么优先选择使用Worker实现吧。由于有些平台不支持类似Worker的API,那么可选的方案是将算法分解为多个步骤。代码如下:
//首先,将逻辑分为几个步骤
function step1(){console.log(1);}
function step2(){console.log(2);}
function step3(){console.log(3);}
var taskArr = [step1, step2, step3];
var doWork = function(tasks){
function next(){
if(tasks.length === 0){
console.log('Tasks finished.');
return;
}
var task = tasks.shift();
if(task){
task();
setTimeout(next, 0);
}
}
setTimeout(next, 0);
}
//启动任务
doWork(taskArr);
Tips:
先看一个简单的示例:
function downFiles(urls){
var result = [],len = urls.length;
if(len === 0){
console.log('urls argument is a empty array.');
return;
}
urls.forEach(function(url){
downloadAsync(url, function(text){
result.push(text);
if(result.length === len){
console.log('download all files.');
}
});
});
}
有什么问题呢?result的结果和urls是顺序并不匹配,所以,我们不知道怎么使用这个result。
如何改进?请看如下代码,使用计数器,代码如下:
function downFiles(urls){
var result = [],len = urls.length;
var count = 0;// 定义计数器
if(len === 0){
console.log('urls argument is a empty array.');
return;
}
urls.forEach(function(url, i){
downloadAsync(url, function(text){
result[i] = text;
count++;
//计数器等于url个数,那么退出
if(count === len){
console.log('download all files.');
}
});
});
}
Tips:
如果异步下载代码,优先从缓存拿数据,那么代码很可能是:
var cache = new Dict();
function downFileWithCache(url, onsuccess){
if (cache.has(url)){
onsuccess(cache.get(url));
return;
}
return downloadAsync(url, function(text){
cache.set(url, text);
onsuccess(text);
});
}
以上代码,同步的调用了回调函数,可能会导致一些微妙的问题,异步的回调函数本质上是以空的调用栈来调用,因此将异步的循环实现为递归函数是安全的,完全没有累计赵越调用栈控件的危险。同步的调用不能保证这一点,所以,更好的代码如下:
var cache = new Dict();
function downFileWithCache(url, onsuccess){
if (cache.has(url)){
setTimeout(onsuccess.bind(null, cache.get(url)), 0)
return;
}
return downloadAsync(url, function(text){
cache.set(url, text);
onsuccess(text);
});
}
Tips:
一直以来,JavaScript处理异步的方式都是callback,当异步任务很多的时候,维护大量的callback将是一场灾难。所以Promise规范也应运而生,http://www.ituring.com.cn/article/66566 。
Promise已经纳入了ES6,而且高版本的Chrome、Firefox都已经实现了Promise,只不过和现如今流行的类Promise类库相比少些API。
看下最简单的Promise代码(猜猜最后输出啥?):
var p1 = new Promise(function(resolve, reject){
setTimeout(function(){
console.log('1');
resolve('2');
}, 3000);
});
p1.then(function(val){
console.log(val);
});
如果代码是这样呢?
var p1 = new Promise(function(resolve, reject){
setTimeout(function(){
console.log('1');
//resolve('2');
reject('3');
}, 3000);
});
p1.then(function(val){
console.log(val);
}, function(val){
console.log(val);
});
再来看一个Promise.all的示例:
Promise.all([new Promise(function(resolve, reject){
setTimeout(function(){
console.log(1);
resolve(1);
}, 2000);
}), new Promise(function(resolve, reject){
setTimeout(function(){
console.log(2);
resolve(2);
}, 1000);
}), Promise.reject(3)])
.then(function(values){
console.log(values);
});
Promise.all([]).then(fn)
只有当所有的异步任务执行完成之后,才会执行then。
接着看一个Promise.race的示例:
Promise.race([new Promise(function(resolve, reject){
setTimeout(function(){
console.log('p1');
resolve(1);
}, 2000);
}), new Promise(function(resolve, reject){
setTimeout(function(){
console.log('p2');
resolve(2);
}, 1000);
})])
.then(function(value){
console.log('value = ' + value);
});
结果是:
p2
value = 2
p1
Promise.race([]).then(fn)
会同时执行所有的异步任务,但是只要完成一个异步任务,那么就调用then。
promise.catch(onRejected)是promise.then(undefined, onRejected) 的语法糖。
更多关于Promise的资料请参考:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise
第三方Promise库有许多,如:Q, when.js 等