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 等