在寫JavaScript程式時,常有協調非同步作業的需求,像是作業B必須等待作業A完成後再進行,傳統做法多使用Callback函式,例如:

function doJobA(callback) {
  //do something
  callback();
}
function doJobB() {
  //do another thing
}
doJobA(doJobB);

學會jQuery.Deferred後,我幾乎都改用Deferred處理非同步作業,不管串接多個作業循序執行[參考]或等待多項作業完成再進行,用Deferred都不難搞定。但有一些例外情況:等待對象不在我們掌握範圍(例如:位於其他Frame、由3rd Party程式控制),或是在完成時加入Callback呼叫的修改工程浩大,我多會選擇另一條捷徑-輪詢(Polling)大法!使用setInterval每隔一小段時間(例如:0.05秒)檢查前置作業是否完成,一發現完成就觸發後續作業。走捷徑要付點代價,輪詢法有兩項缺點:1) 即時性較差,前置作業完成後要等到下次檢查才觸發後續作業,最糟會延遲一次輪詢的時間間隔 2) 前置作業完成前的反覆檢查需消耗CPU資源(故要切記,檢查邏輯愈簡單愈好,例如複雜的jQuerySelector或AJAX查詢就不是好選擇)。雖然有這些缺點,但輪詢法很單純直覺,且能用在所有非同步作業上,在我心中就像散彈槍一樣好用,不管叢林裡有什麼妖魔鬼怪,不需費心瞄準,轟一聲,問題就解決了。 XD

因為三不五時拿出來打怪,索性寫成共用函式-jQuery.waitFor()方法。基於應用彈性,我設計了兩個參數:等待時限(timeout)以及輪詢間隔(checkInterval)。timeout用來停損,避免前置作業故障時還一直輪詢到海枯石爛;checkInterval則用來控制檢查頻率,間隔愈短即時性愈高,但愈消耗CPU,可視需求自行拿捏。寫非同步程式我已經離不開jQuery Deferred,所以jQuery.waitFor()骨子裡還是用jQuery Deferred實做,傳回的是Promise物件,接上done()指定後續作業,fail()則用來處理等待逾時。完整程式及應用範例如下:demo

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>waitFor函式</title>
</head>
<body>
  <input type="button" id="b1" value="Start (5000ms)" />
  <input type="button" id="b2" value="Start (1000ms)" />
  <hr />
  <input type="button" id="trg" value="Trigger" />
  <hr />
  <ul id="display"></ul>
  
  
<script src="//code.jquery.com/jquery-2.1.1.min.js"></script> 
  <script>
/**
 * 條件成立時才執行
 *
 * @param check 檢查函式
 * @param timeout 最長等待時間
 * @param checkInterval 檢查間隔
 */
jQuery.waitFor = function(check, timeout, checkInterval) {
  var dfd = jQuery.Deferred();
  var checkHandle = setInterval(function () {
    if (check()) {
      clearInterval(checkHandle);
      dfd.resolve();
    }
  }, checkInterval || 50);
  var timeoutHandle = setTimeout(function () {
    if (dfd.state() == "pending") {
      clearInterval(checkHandle);
      clearTimeout(timeoutHandle);
      dfd.reject();
    }
  }, timeout || 5000);
  return dfd.promise();
} 
 
//測試程式
var $disp = $("#display");
function log(msg) {
  var t = JSON.stringify(new Date()).split('T')[1].substr(0, 12);
  $disp.append("<li>" + t + " " + msg + "</li>")
}
var state = false;
function init() {
  state = false;
  log("Start");
}
function trigger() {
  state = true;
  log("Trigger");
}
$("#b1").click(function() {
  init();
  $.waitFor(function() { return state; }, 5000)
  .done(function() { log("Executed"); }).fail(function() { log("Timeout"); });
});
$("#b2").click(function() {
  init();
  $.waitFor(function() { return state; }, 1000)
  .done(function() { log("Executed"); }).fail(function() { log("Timeout"); });
});
$("#trg").click(trigger);
  </script>
</body>
</html>

以下為操作示範。每次測試時會先將state變數設為false,接著呼叫我們自訂的jQuery.waitFor(fuction() { return state; }),每隔50ms檢查一次state,當state為true就觸發done()邏輯,若超過1秒或5秒state都還是false,則判定逾時觸發fail()邏輯,而「Trigger」按鈕則用來將state變數設為true。在測試中,先測Timeout,按鈕後什麼都不做,在5秒及1秒後會出現Timeout訊息;第二回合則驗證一按下「Trigger」鈕,done()立即執行。

以上就是我的散彈槍製作手冊,祝大家打怪順利~ XD


Comments

Be the first to post a comment

Post a comment