2015年12月13日 星期日

jQuery之Deffered的應用

jQuery有Deferred及Promise物件,可以幫助我們處理異步函式的callback順序關係。
在這邊舉兩個例子來說明其可能的兩個應用:

一、用Deferred實現異步函式的callback順序關係
Deferred可以實現異步函式的callback順序關係,在下例中,我們有三個函式,f1()、f2()、f3(),其中都有setTimeout模擬一秒的異步延遲,並且希望能f1做完才執行f2、f2做完才執行f3。如果我們沒有處理好callback的順序關係的話,就會發生f1的setTimeout還沒執行完就執行f2的情況。

在此例中,我們利用Deferred.then()來確保f1,f2,f3的執行順序關係,在f1,f2,f3中,會個自建立一個Deferred物件並傳回。
而setTimeout結束後,會呼叫Deferred.resolve()來告知函式執行完成,並可以選擇送進一個參數(param)來給之後的callback函式使用,接著會執行then()裡面的函式。

以上說明是簡單的行為了解。其實真正的實作比較複雜,可以參考jQuery之Deferred的原理,在這邊稍微講一下大概的背後過程:

  1. f1()回傳了第一個Deferred物件。
  2. f1('f1').then(f2)建立了第二個Deferred物件,並為第一個Deferred物件設定done(ff2),ff2是callback函式,ff2的內容是執行f2(),並取得f2()回傳的第三個Deferred物件,接著設定第三個物件的done(fff2),其中fff2=第二個Deferred物件.resolve。
  3. 所以當呼叫then(f2)時,會回傳一個Deferred物件(即第二個Deferred物件。
  4. 當f1()裡呼叫defer.resolve()時,會執行f2()。
  5. 當f2()裡呼叫defer.resolve()時,會引發第三個Deferred物件的done(),接著就會引發第二個Deferred物件的resolve()。
  6. 以此類推,第二個Deferred物件被resolve()後就會去執行f3()。


HTML:
<div id='result'>
  Result:
</div>
Javascript:
function f1(param) {
  var defer = $.Deferred();
  setTimeout(function() {
    $('#result').html($('#result').html() + '<br/>f1 got parameter : ' + param);
    //將'f2'當參數傳給don()或then()之後的function
    defer.resolve('f2');
  }, 1000);
  return defer;
}

function f2(param) {
  var defer = $.Deferred();
  setTimeout(function() {
    $('#result').html($('#result').html() + '<br/>f2 got parameter : ' + param);
    //將'f3'當參數傳給don()或then()之後的function
    defer.resolve('f3');
  }, 1000);
  return defer;
}

function f3(param) {
  var defer = $.Deferred();
  setTimeout(function() {
    $('#result').html($('#result').html() + '<br/>f3 got parameter : ' + param);
    //defer.resolve(param + ' f3');
  }, 1000);
  return defer;
}
//Deferred參數的傳遞
f1('f1').then(f2).then(f3);
二、合併多個異步函式的callback處理,即個別異步函式都執行完後才執行callback

在此例中我們用到了$.when(),它可以被傳入多個Deferred或Promise物件,並在所有的Deferred或Promise物件都被resolve()或reject()後才執行callback。

在這裡我們有三個動畫,都為一串文字由右往左移動,每個動畫的時間都不一樣,第一個是用CSS的動畫,其他兩個是用jQuery的animate()做的動畫。

對於每個動畫,我們都建立一個新的Deferred物件給它,並在動畫執行完後將它得到的Deferred給resolve()。而每個Deferred物件我們都放進一個叫des的Array中。

最後我們把Array裡面的三個Deferred物件丟給$.when()裡當參數,利用then()或done()來設定三個Deferred物件都resolve()後才要執行的callback。

P.S.

  1. 因為$.when()不能接受Array型式的參數,所以這裡利用了apply($,des)來將des中的內容傳給when(),其中第一個參數為要代替when()中的this的參考,第二個參數是一個Array,其內容為要傳給when()的參數。
  2. resolveDeferred()接受一個Deferred物件,並傳回一個function,在傳回的function中其this指向呼叫resolveDeferred的物件;而deferred指向function被建立的域(即resolveDeferred())中的deferred。(可參見閉包(Closure))


HTML:
<div class='animSection'>
  <div id='anim1'>Watch me move1</div>
  <div id='anim2'>Watch me move2</div>
  <div id='anim3'>Watch me move3</div>
</div>
<br/>
<div id='text' class="text">Detect Result:</div>
CSS:
.animSection div{
  padding-left: 100%;
}

.animClass {
  animation-name: myAnim;
  animation-duration: 6s;
  animation-fill-mode: forwards;
}

@keyframes myAnim {
  0% {
    padding-left: 100%;
  }
  100% {
    padding-left: 0%;
  }
}
Javascript:
var des = []; //用來放Deferred或Promise物件的array
var deferred;

deferred = $.Deferred();
des.push(deferred.promise()); 
//也可以寫成des.push(deferred),deferred.promise()回傳的為Promise物件,為不可修改狀態只供查詢狀態的物件,即沒有resolve()等方法
//
$('#anim1').addClass('animClass').one('animationend', resolveDeferred(deferred));

deferred = $.Deferred();
des.push(deferred.promise());
$('#anim2').animate({
  paddingLeft: "-=100%"
}, 9000, resolveDeferred(deferred));

deferred = $.Deferred();
des.push(deferred.promise());
$('#anim3').animate({
  paddingLeft: "-=100%"
}, 5000, resolveDeferred(deferred));
//因為$.when()只能接收一個一個用逗號分開的參數,所以利用apply來將array以個別
//參數的方式傳給when(),when()可接受Deferred及Promise物件
$.when.apply($, des).done(function() {
  $('#text').html($('#text').html() + '<br/>All finished!');
});
//也可以寫成以下,then可以給兩個參數,第一個為done()的callback函式,
//第二個為fail()的callback函式
//$.when.apply($, des).then(function(){
//	$('#text').html($('#text').html() + '<br/>All finished!');
//});

function resolveDeferred(deferred) {
  return function() {
    $('#text').html($('#text').html() + '<br/>#' + this.id + ' finished!');
    deferred.resolve();
  }
}

2015年12月12日 星期六

jQuery之Deferred的原理(參看源碼)

了解了jQuery的Callbacks以後,我們可以藉由觀看jQuery的源始碼(jquery-1.11.3.js)來較清楚地了解Deferred到底做了什麼事及怎麼實現的,以更好的運用Deferred及其相關API。

首先,我們先把Deferred的jQuery源碼貼出來:

 Deferred: function( func ) {
  var tuples = [
    // action, add listener, listener list, final state
    [ "resolve", "done", jQuery.Callbacks("once memory"), "resolved" ],
    [ "reject", "fail", jQuery.Callbacks("once memory"), "rejected" ],
    [ "notify", "progress", jQuery.Callbacks("memory") ]
   ],
   state = "pending",
   promise = {
    state: function() {
     return state;
    },
    always: function() {
     deferred.done( arguments ).fail( arguments );
     return this;
    },
    then: function( /* fnDone, fnFail, fnProgress */ ) {
     var fns = arguments;
     return jQuery.Deferred(function( newDefer ) {
      jQuery.each( tuples, function( i, tuple ) {
       var fn = jQuery.isFunction( fns[ i ] ) && fns[ i ];
       // deferred[ done | fail | progress ] for forwarding actions to newDefer
       deferred[ tuple[1] ](function() {
        var returned = fn && fn.apply( this, arguments );
        if ( returned && jQuery.isFunction( returned.promise ) ) {
         returned.promise()
          .done( newDefer.resolve )
          .fail( newDefer.reject )
          .progress( newDefer.notify );
        } else {
         newDefer[ tuple[ 0 ] + "With" ]( this === promise ? newDefer.promise() : this, fn ? [ returned ] : arguments );
        }
       });
      });
      fns = null;
     }).promise();
    },
    // Get a promise for this deferred
    // If obj is provided, the promise aspect is added to the object
    promise: function( obj ) {
     return obj != null ? jQuery.extend( obj, promise ) : promise;
    }
   },
   deferred = {};

  // Keep pipe for back-compat
  promise.pipe = promise.then;

  // Add list-specific methods
  jQuery.each( tuples, function( i, tuple ) {
   var list = tuple[ 2 ],
    stateString = tuple[ 3 ];

   // promise[ done | fail | progress ] = list.add
   promise[ tuple[1] ] = list.add;

   // Handle state
   if ( stateString ) {
    list.add(function() {
     // state = [ resolved | rejected ]
     state = stateString;

    // [ reject_list | resolve_list ].disable; progress_list.lock
    }, tuples[ i ^ 1 ][ 2 ].disable, tuples[ 2 ][ 2 ].lock );
   }

   // deferred[ resolve | reject | notify ]
   deferred[ tuple[0] ] = function() {
    deferred[ tuple[0] + "With" ]( this === deferred ? promise : this, arguments );
    return this;
   };
   deferred[ tuple[0] + "With" ] = list.fireWith;
  });

  // Make the deferred a promise
  promise.promise( deferred );

  // Call given func if any
  if ( func ) {
   func.call( deferred, deferred );
  }

  // All done!
  return deferred;
 }

再來一步步解析:

jQuery的Callbacks

jQuery有一個對控制異步函式時很好用的類別Deferred,還有它的簡少API版本,Promise(無法更改state)。

我們可以藉由觀看jQuery的源始碼(jquery-1.11.3.js)來較清楚地了解Deferred到底做了什麼事及怎麼實現的,以更好的運用Deferred及其相關API。

在了解Deferred之前,我們可以先來了解Callbacks這個類別。在這邊用形象的方式來了解Callbacks的運作方式。

Callbacks可以被當成是一個佇列式的列隊(先進先出,FIFO),我們可以把想要做的function傳進列隊中(add()),並在想要的時候觸發列隊中的function執行(fire() or fireWith()),在這邊是一次執行所有的function,雖然有照順序開始執行,但並無法保證一個function執行完才執行下一個(例如要花時間的異步函式)。

而Deferred則是利用Callbacks的性質來實作的,基本上就是預先將異步函式做完時要執行的回調函式先放到列隊中,並執行異步函式,等異步函式執行完後手動決定要觸發列隊裡函式並執行的時機。

jQuery的Callbacks有以下常用的API function
  1. $.Callbacks([flags]) : 創建並返回一個Callbacks類別。
    • flags : 可選,可以接受多個,用空格的String來表示,例如有以下幾種:
      •  once:
        • 列隊只能被觸發一次(fire)。
      •  memory:
        • 列隊觸發後,一有函式放進(add)列隊會被馬上執行。
      •  unique:
        • 列隊中的同樣函式(指的是其參考一樣的 reference)再一次觸發時只會被執行一次。
  2. Callbacks.add(callbacks) : 將callbacks函式放進Callbacks列隊中。
  3. Callbacks.fire([arguments]) : 觸發列隊執行其中的函式。
    • arguments : 可選,可傳入參數給列隊中各函式當input value。
  4. Callbacks.fireWith([context] [, arguments]) : 跟fire()相似,也是觸發列隊執行其中的函式,不過可以指定上下文(context),即函式中的this指誰。
  5. Callbacks.disable() : 將callback列隊disable掉,即之後不能在做fire()之類的呼叫(呼叫了也不會有行為)
範例 :

2015年12月5日 星期六

CSS與jQuery的動畫(animation)比較

jQuery與CSS都能實現對網頁上的DOM進行動畫特效,但兩者有一些差異,特別在這邊紀錄:

jQuery的動畫:

  1. 可使用$(selector).animate(styles,speed,easing,callback)來進行動畫,為有數字參數的css屬性進行動畫(無法用於只有字串值的css屬性,例如background-color:red)。
  2. 可使用$(selector).stop(stopAll,goToEnd)來停止動畫,可以只停止當前動畫並繼續 queue中的其他動畫。也可以停止當前動畫並情空動畫列隊(只限queue中的animation列隊成員)(參見queue),此時可以選擇直接將元素(DOM element)的動畫效果跳至當前動畫的最後狀態。
  3. 沒有暫停的方法(pause),即無法中途暫停,之後再從暫停的地方播放剩餘的動畫(resume)。目前要有pasue、resume的方法只能用plug-in等的方式解決,這裡有一些還沒試過的plug-in可以參考: PausejQuery-FxQueues
  4. 動畫結束後並不會引發animation事件。
CSS的動畫
  1. 為DOM element加上附有動畫屬性的CSS來實現動畫。
  2. 在CSS中使用animation-name屬性來指定keyframes (參見CSS3 Animations)。故無法同時在同一個DOM element上指定兩個animation-name的CSS屬性,會被最後決定的屬性覆蓋過去,即一次只能一個動畫。
  3. 可以設定許多不同的屬性來達到不同的效果。例如用animation-iteration-count來設定重覆次數
  4. 可以使用animation-play-state來設定運行(running)、暫停(paused),pased及running可以互相切換,動畫不會被停止無法繼續播(即paused之後再running等同resume效果)。
  5. 動畫播放完以後nimation-play-state會被設為paused。
  6. 動畫無法停止
  7. animation-timing-function可以設定時間與output效果的關係(參見<timing-function>),常用的有可以用來做逐幀動畫的steps(number_of_steps, direction),作用為設定output效果為步進函數(step function),number_of_steps代表切割動畫(動畫指瀏覽器算出來的漸進動畫output效果)為幾幀,direction代表步進的不連續點為每幀的start處還是end處(參見<timing-function>CSS3 timing-function: steps() 詳解小tip: CSS3 animation漸進實現點點點等待提示效果)。
  8. 動畫結束後發出animationend事件,可用JavaScript的addEventListener('animationend',callbackFunction)或jQuery的bind('animationend',callbackFuncion)來獲取事件,但是如果動畫class被移除或animation-play-state被設成paused並不會引發animationend事件。

JavaScript, jQuery 的Event機制 (Capturing, Bubbling)

在Javascript的W3C標準中,當Evnet(事件)發生時,例如click事件、animationEnd事件、hover事件等,Event會以由外到內(Capturing)、再由內到外(Bubbling)的方式傳遞。

我們可以用JavaScript的方法,addEventListener(eventName , callbackFunction , capturOrBubble)
來設置Evnet的監聽器,其中

第一個參數eventName為一個String,表示事件名稱,跟jQuery的bind()及one()不同,不可用空白分隔多個事件。

第二個參數是一個callback方法,可以選擇傳入event參數來得到event資訊,例如可以用event.target或event.srcElement (IE適用) 來得到Event作用的JavaScript對像。

我們也可以用Jquery的方法,bind(eventName,callbackFunction),來做到一樣的事情,不過要要注意bind()的callbackFunction傳入的event不是JavaScript的Event物件,而是被包裹成jQuery版的Event物件,雖然一樣有event.target,但是確沒有event.srcElement,如果要拿到JavaScript版的Event物件,可以使用event.origianlEvent來得到。

現在用下面這個例子來說明:

html:

<div id='1'>
  <div id='2'>
    <div id='3' class='animClass'>Watch me move</div>
  </div>
</div>
<div id='text'></div>


CSS :
.animClass {
  animation-name: myAnim;
  animation-duration: 1s;
}



@keyframes myAnim {
  0% {
    padding-left: 100%;
  }
  100% {
    padding-left: 0%;
  }
}

JavaScript (使用addEventListener) :

document.getElementById('1').addEventListener('animationend', function (event) {
  $('#text').html($('#text').html() + '#1 catched animation end from #' + event.target.id + '! (Capturing)</br>');
}, true);

document.getElementById('2').addEventListener('animationend', function (event) {
  $('#text').html($('#text').html() + '#2 catched animation end from #' + event.target.id + '! (Capturing) </br>');
}, true);

document.getElementById('3').addEventListener('animationend', function (event) {
  $('#text').html($('#text').html() + '#3 catched animation end from #' + event.target.id + '! (Capturing) </br>');
}, true);

document.getElementById('1').addEventListener('animationend', function (event) {
  $('#text').html($('#text').html() + '#1 catched animation end from #' + event.target.id + '! (Bubbling)</br>');
}, false);

document.getElementById('2').addEventListener('animationend', function (event) {
  $('#text').html($('#text').html() + '#2 catched animation end from #' + event.target.id + '! (Bubbling) </br>');
}, false);

document.getElementById('3').addEventListener('animationend', function (event) {
  $('#text').html($('#text').html() + '#3 catched animation end from #' + event.target.id + '! (Bubbling) </br>');
}, false);

其結果是: