No.6一、不要阻塞I/O事件队列html
Tips:node
JavaScript程序是构建在事件之上的。在其余一些语言中,咱们可能经常会实现以下代码:git
var result = downFileSync('http://xxx.com'); console.log(result);
以上代码,若是downFileSync须要5分钟,那么程序就会停下来等待5分钟。这样的函数就被称为同步函数(或阻塞函数)。若是在浏览器中实现这样的函数,那么结果就是浏览器卡住,等待下载完成后,再继续响应。那么,这将极大的影响体验。因此,在JavaScript中,通常使用以下方式:github
downFileAsync('http://xxx.com', function(result){ console.log(result); }); console.log('async');
以上代码执行中,就算下载文件要5分钟,那么程序也会立马打印出“async”,而后在下载完成的时候,打印result出来。这样才能保证执行环境能正确响应客户的操做。web
JavaScript并发的一个最重要的规则是毫不要在应用程序事件队列中使用阻塞I/O的API。在浏览器中,甚至基本没有任何阻塞的API是可用的。其中XMLHttpRequest库有一个同步版本的实现,被认为是一种很差的实现,影响Web应用程序的交互性。算法
在现代浏览器(IE10+(含)、Chrome、FireFox)中,提供了Worker的API,该API使得产生大量的并行计算称为可能。数据库
如何使用?canvas
首先,编写两个文件,第一个是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,代码以下:promise
// 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 等