編寫高質量JS代碼的68個有效方法(十三)

No.6一、不要阻塞I/O事件隊列html

Tips:node

  1. 異步API使用回調函數來延緩處理代價高昂的操做以免阻塞主應用程序
  2. JavaScript併發的接收事件,但會使用一個事件隊列按序地處理事件處理程序
  3. 在應用程序事件隊列中毫不要使用阻塞的I/O

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的優缺點,咱們能夠作什麼:

  1. 能夠加載一個JS進行大量的複雜計算而不掛起主進程,並經過postMessage,onmessage進行通訊
  2. 能夠在worker中經過importScripts(url)加載另外的腳本文件
  3. 可使用 setTimeout(), clearTimeout(), setInterval(), and clearInterval()
  4. 可使用XMLHttpRequest來發送請求
  5. 能夠訪問navigator的部分屬性

有那些侷限性:

  1. 不能跨域加載JS
  2. worker內代碼不能訪問DOM
  3. 各個瀏覽器對Worker的實現不大一致,例如FF裏容許worker中建立新的worker,而Chrome中就不行
  4. 不是每一個瀏覽器都支持這個新特性

更多信息,請參考:

  1. https://developer.mozilla.org/zh-CN/docs/Web/Guide/Performance/Usingwebworkers
  2. http://www.cnblogs.com/feng_013/archive/2011/09/20/2175007.html

No.6二、在異步序列中使用嵌套或命名的回調函數

Tips:

  1. 使用嵌套或命名的回調函數按順序地執行多個異步操做
  2. 嘗試在過多的嵌套的回調函數和尷尬的命名的非嵌套回調函數之間取得平衡
  3. 避免將可被並行執行的操做順序化

想象一下以下需求,異步請數據庫查找一個地址,並異步下載。因爲是異步,咱們不可能發起兩個連續請求,那麼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。

No.6三、小心丟棄錯誤

Tips:

  1. 經過編寫共享的錯誤處理函數來避免複製和粘貼錯誤處理代碼
  2. 確保明確地處理全部的錯誤條件以免丟棄錯誤

通常狀況下,咱們的錯誤處理代碼以下:

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,這已經成爲一個大衆標準

No.6四、對異步循環使用遞歸

Tips:

  1. 循環不能是異步的
  2. 使用遞歸函數在時間循環的單獨輪次中執行迭代
  3. 在事件循環的單獨倫次中執行遞歸,並不會致使調用棧溢出

針對異步下載文件,若是要使用循環,大概是以下代碼:

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);// 啓動遞歸
}

相似這樣的實現,就能解決批量下載的問題了。

No.6五、不要再計算時阻塞事件隊列

Tips:

  1. 避免在主事件隊列中執行代碼高昂的算法
  2. 在支持Worker API的平臺,該API能夠用來在一個獨立的事件隊列中運行長計算程序
  3. 在Worker API 不可用或代價高昂的環境中,考慮將計算程序分解到事件循環的多個輪次中

打開瀏覽器控制檯,執行 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);

No.6六、使用計數器來執行並行操做

Tips:

  1. JavaScript應用程序中的事件發生是不肯定的,即順序是不可預測的
  2. 使用計數器避免並行操做中的數據競爭

先看一個簡單的示例:

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.');
      }
    });
  });
}

No.6七、毫不要同步地調用異步的回調函數

Tips:

  1. 即便能夠當即獲得數據,也毫不要同步地調用異步回調函數
  2. 同步地調用異步的回調函數擾亂了預期的操做序列,並可能致使意想不到的交錯代碼
  3. 同步地調用異步的回調函數可能致使棧溢出或錯誤的處理異常
  4. 使用異步的API,好比setTimeout函數來調用異步回調函數,使其運行於另一個回合

若是異步下載代碼,優先從緩存拿數據,那麼代碼極可能是:

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);
  });
}

No.6八、使用promise模式清潔異步邏輯

Tips:

  1. promise表明最終值,即並行操做完成時最終產生的結果
  2. 使用promise組合不一樣的並行操做
  3. 使用promise模式的API避免數據競爭
  4. 在要求有意的競爭條件時使用select(也被稱爲choose)

一直以來,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 等

相關文章
相關標籤/搜索