第二十九課:javascript異步處理


大家知道javascript中有多少方法能夠實現異步處理嗎?setTimeout(),setInterval()是最常用的兩個。XMLHttpRequest對象,進行ajax請求時。postMessage()進行跨域操作時。WebWorker創建新的線程時。setImmediate方法(新的setTimeout方法)。requestAnimationFrame進行動畫操作時。這些東西都有一個共同的特點,就是擁有一個回調函數。有的異步API還提供了相對應的中斷API,比如:clearTimeout,clearInterval,clearImmediate,cancelAnimationFrame。

早些年,我們就是通過setTimeout和setInterval在網頁上實現動畫的,這種動畫其實就是通過異步API不斷的調用同一個回調方法實現的,回調方法里面對元素節點的某些樣式進行很小范圍的改動。

首先,我們來講一下setTimeout和setInterval這兩個API,這里只講它不常見的知識點:

(1)它們的回調方法,如果執行時間大於間隔時間(比如:setInterval(function(){里面執行代碼的事件大於50毫秒},50)),那么實際上的間隔時間會大於50毫秒(因為js執行線程其實是一個隊列,它執行完一個函數后,才會執行另外一個函數,因此,這段代碼的意思是:每隔50毫秒,給執行線程添加一個函數,如果執行線程在執行這個函數時,后面又來了一個函數,后面這個函數會在那里排隊,要等前面那個函數執行完成,它才會執行,因此實際上函數執行的間隔時間大於50毫秒)。

(2)它們存在一個最小的時鍾間隔,IE6-8下為15.6ms,IE9為10ms,IE10和其他標准瀏覽器為4ms。意思就是說,如果你setInterval(functiojn(){},1),這個代碼的意思是一毫秒就執行一次function,但是實際上,瀏覽器它們有一個最小的間隔,即便你寫了1ms,它也會按照它的這個最小事件間隔來執行function(比如:IE6-8會15.6毫秒才執行一次function)。如果你覺得IE6-8下,最短時鍾間隔太大,你可以利用image死鏈時立即執行onerror回調的情況進行改造,比如:

var orig_setTimeout = window.setTimeout;

window.setTimeout = function(callback,time){

  if(time > 15) {

    orig_setTimeout(callback,time);

  }

  else{   //當間隔時間小於15毫秒時,就新建一個Image對象,給它一個錯誤的src,這時會立即調用onerror回調方法,這時就會執行callback,實現間隔時間小於15毫秒的功能。

    var img = new Image();

    img.onload = img.onerror = function(){

      callback();

    };

    img.src = "data:,foo";

  }

}

(3)不寫第二個參數時,瀏覽器自動分配時間,IE,Firefox中,第一個分配可能給個100ms,往后會慢慢縮小到最小時鍾間隔。Safari,Chrome,Opera則分配一個10ms。Firefox中,setInterval不寫第二個參數,會當做setTimeout處理,只執行一次。

(4)IE10+和標准瀏覽器支持額外參數,從第三個參數起,作為回調的傳參傳入。比如:setTimeout(function(){},1000,1,2,4),那么function中的[].slice.call(arguments) = [1,2,4]。IE6-9可以這樣模擬:

if(IE9-){

  (function(overrideFun){

    window.setTimeout = overrideFun(window.setTimeout);

    window.setInterval = overrideFun(window.setInterval);

  })

  (

    function(originalFun){

      return function(callback, delay){

        var args = [].slice.call(arguments,2);  //從第三個參數開始取

        return originalFun(function(){

          if(typeof callback == "string"){  //如果第一次參數傳入的是字符串

            eval(callback);

          }else{

            callback.apply(this,args);   //把參數傳入回調方法

          }

        },delay);

      };

    }

  )

}

(5)setTimeout方法的事件參數若為負數或0或極大的正數,標准瀏覽器都立即執行,而老版本的IE處理會出現較大的差異,不用研究。

接下來,我們來講解下Deferred對象(jQuery中的ajax異步處理對象)的前身Mochikit Deferred。

Deferred是當今最著名的異步模型,它原來是Python的Twisted框架的一個類,后來被Mochikit框架引進來,后面被dojo抄去,jQuery后面也引進。我們來詳細講一下Mochikit Deferred的實現原理(接下來的Deferred指的是Mochikit Deferred):

Deferred內部把回調分成兩種,一種是成功回調,用於正常時執行,一種叫錯誤回調,用於出錯時執行。各自組成兩個隊列,我們可以叫做成功隊列與錯誤隊列。在添加回調時,它是一組一組(成功回調和錯誤回調)的添加的,每組的回調只會執行一個(不是執行成功回調,就是執行錯誤回調),每組回調接收到的參數,都是上一組回調處理后返回的結果,只有第一次組的回調接收的參數是用戶傳入的。那么如何決定是執行成功回調還是錯誤回調呢,也是根據上一組的結果決定的,如果上一組的結果拋出錯誤,那么這一組就會執行錯誤回調,如果這一組的錯誤回調不拋出錯誤,那么下一組的回調就執行成功回調。第一組的執行是用戶決定的,意思就是用戶調用成功回調的方法,就執行成功回調,用戶執行錯誤回調的方法就執行錯誤回調。

我們先來看一下Deferred里面的方法:

addCallback 添加成功回調的方法。

addErrback 添加錯誤回調的方法

addBoth 同時添加正常回調和錯誤回調的方法

這三個方法內部都會調用addCallbacks方法,而這個方法的參數只能是兩個函數或一個函數一個null。也就是說上面的三個方法會把參數轉換成兩個函數或一個函數一個null,然后傳給addCallbacks方法。Deferred實例有一個chain數組屬性,數組中的每一項都是一個雙元素的數組,比如:

deferred.chain = [ [callback,errorcallback], [callback1,errorcallback1], [callback2,errorcallback2] ];

舉個例子:

var d = new Deferred();

d.addCallback(myCallback);

d.addErrback(myErrback);

d.addBoth(myBoth);

d.addCallbacks(myCallback,myErrback);

這時,d.chain = [ [myCallback, null], [null, myErrback], [myBoth, myBoth], [myCallback, myErrback] ];

觸發這些回調是通過調用d.callback和d.errback方法實現的。這兩個方法里面的流程是一致的,首先檢查此Deferred對象有沒有被調用過,如果沒有,就調用_resback方法。

 當然用戶可以在callback或errback中傳入參數,傳入的參數在_resback方法中會生成一個數組,如果調用的是callback方法(成功的回調),那么就把參數放到數組的第一個位置,如果調用的是errback方法(失敗的回調),那么就把參數放到數組的第二個位置。然后_resback方法會判斷執行有沒有被切斷(異步過程有沒有被終止),沒有的話,就調用_fire方法執行回調。

_fire方法就是不斷彈出chain數組中的一組函數,根據狀態取第一個回調還是第二個回調(第一個是成功回調,第二個是失敗回調)執行,每一組回調都接收上一組回調的返回值作為參數。舉個例子:

function increment(value){

  console.log(value);

  return value+1;

}

var d = new Deferred();

d.addCallback(increment);     //d.chain = [[increment,null]]

d.addCallback(increment);   //d.chain = [[increment,null],[increment,null]]

d.addCallback(increment);   //d.chain = [[increment,null],[increment,null],[increment,null]]

d.callback(1);   //_resback(1)->  [1,null]  -> fire([1,null])  ->  循環取出d.chain中的每一組函數, 因為傳入的數組第一項是1,就代表成功回調,因此執行第一組函數的成功回調increment方法,這時打印出1,返回2,而這個2會繼續傳給第二組函數,因為也是成功回調,所以就執行第二組函數的成功回調increment方法,打印出2,返回3。以此類推。

那么失敗回調什么時候執行呢,在以上的執行流程中,會有一個try catch,如果回調方法拋出錯誤,就會catch住,然后執行下一組函數中的失敗回調。舉個例子:

var d = new Deferred();

d.addCallback(function(a){ console.log(a);return 4}).addBoth(function(a){console.log(a);throw "拋錯"},function(b){console.log(b);return "xx"}).addBoth(function(a){console.log(a); return "正常"},function(b){console.log(b);return "出錯"}).addBoth(function(a){console.log(a + "正常")},function(b){console.log(b+"繼續出錯")})

d.callback(3);

因為調用的是callback方法所以是成功回調,因此打印出a(也就是3),返回4給下一組函數,下一組函數收到這個4,因為沒有拋出錯誤,所以是成功回調,因此打印出4,throw "拋錯",這時下一組函數,就會執行失敗回調,也就是function(b){console.log(b);return "出錯"},打印出Error:拋錯,返回"出錯",這時沒有拋出錯誤,而是正常返回"出錯"這個字符串,因此下一組函數,就會執行成功回調,也就是function(a){console.log(a + "正常")},這時打印出"出錯正常"。

在Mochikit中,Deferred還有一個重要的功能就是,可以並歸多個Ajax的請求結果,然后再做處理。它是使用DeferredList實現的。早期的jQuery和Prototype沒有這個東西,它們之前是使用計數器實現的,非常復雜。舉個例子:

有一個業務,需要發起4個Ajax請求,這4個請求的地址和返回時間不一樣,必須等到它們都處理完成后,整合它們4個的返回數據,然后根據這個整合的數據再發起兩個ajax請求,等到這兩個ajax請求全部處理完成,整合這兩個請求的數據后,再進行一次ajax請求,返回數據才算一次業務處理成功。如果你不使用異步對象來處理,你可以想想你的代碼會寫的多復雜,而且多容易出錯。如果使用DeferredList來實現,非常簡單,先把那4個ajax請求放到DefrredList對象d1中,然后4個ajax全部處理完成后,才會觸發DefrredList對象的回調,而在這個回調中把這4個請求的數據整合,然后根據整合的數據發起兩個ajax請求,這兩個ajax請求又放到一個DefrredList對象d2中,等這兩個ajax請求全部處理完成后,就會執行d2的回調方法,最后在這個回調中整合這2個請求的數據,發送最后一次ajax請求,就OK了。

下一課,將講解JSDeferred對象,它基本奠定了后來稱為Promise/A的范式。

 

 

 

加油!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM