DIV CSS 佈局教程網

 DIV+CSS佈局教程網 >> 網頁腳本 >> JavaScript入門知識 >> JavaScript基礎知識 >> 使用AmplifyJS組件配合JavaScript進行編程的指南
使用AmplifyJS組件配合JavaScript進行編程的指南
編輯:JavaScript基礎知識     

事件分發的作用

在為頁面添加各類交互功能時,我們熟知的最簡單的做法就是為頁面元素綁定事件,然後在事件處理函數中,做我們想要做的動作。就像這樣的代碼:

element.onclick = function(event){
  // Do anything.
};

如果我們要做的動作不復雜,那麼實際邏輯功能的代碼,放在這裡是可以的。如果今後需要修改,再到這段事件處理函數的位置來修改。

再進一步,為了做適當的代碼復用,我們可能會把邏輯功能中的一部分分拆到一個函數內:

element.onclick = function(event){
  // Other code here.
  doSomethingElse();
};

這裡的函數doSomethingElse對應的功能可能會在其他地方用到,所以會這樣做分拆。此外,可能會有設定坐標這樣的功能(假定函數名為setPosition),則還需要用到浏覽器事件對象event提供的諸如指針位置一類的信息:

element.onclick = function(event){
  // Other code here.
  doSomethingElse();
  setPosition(event.clientX, event.clientY);
};

此處有一個不推薦的做法是直接把event對象傳遞給setPosition。這是因為,分清邏輯功能和事件偵聽兩種職責,是一種良好的實踐。只讓事件處理函數本身接觸到浏覽器事件對象event,有利於降低代碼耦合,方便獨立測試及維護。

那麼,功能越來越多,越來越復雜了會怎麼樣呢?如果沿用之前的做法,可能是這個樣子:

element.onclick = function(event){
  doMission1();
  doMission2(event.clientX, event.clientY);
  doMission3();
  // ...
  doMissionXX();
};

雖然這樣用也沒問題,但這種時候其實就可以考慮更優雅的寫法:

element.onclick = function(event){
  amplify.publish( "aya:clicked", {
    x: event.clientX,
    y: event.clientY
  });
};

這種形式就是事件分發,請注意,這裡的事件並不是指浏覽器原生的事件(event對象),而是邏輯層面的自定義事件。上面的aya:clicked就是一個隨便寫(really?)的自定義事件名稱。

顯然到這還沒結束,為了完成之前的復雜的功能,我們還需要將自定義事件和要做的事關聯在一起:

amplify.subscribe( "aya:clicked", doMission1);
// ...
amplify.subscribe( "aya:clicked", doMission2);
// ...

看起來又繞了回來?沒錯,但這是有用的。一方面,浏覽器原生事件的偵聽被分離並固化了下來,以後如果邏輯功能有變化,例如減少幾個功能,則只需要到自定義事件的關聯代碼部分做刪減,而不需要再關心原生事件。另一方面,邏輯功能的調整變得更為靈活,可以在任意的代碼位置通過subscribe添加功能,而且可以自行做分類管理(自定義的事件名)。

簡單來說,事件分發通過增加一層自定義事件的冗余(在只有簡單的邏輯功能時,你就會覺得它是冗余),降低了代碼模塊之間的耦合度,使得邏輯功能更為清晰有條理,便於後續維護。

等下,前面那個出境了好幾次的很有存在感的amplify是干什麼的?

Nice,終於是時候介紹這個了。
AmplifyJS

事件分發是需要一定的方法來實現的。實現事件分發的設計模式之一,就是發布/訂閱(Publish/Subscribe)。

AmplifyJS是一個簡單的JavaScript庫,主要提供了Ajax請求、數據存儲、發布/訂閱三項功能(每一項都可獨立使用)。其中,發布/訂閱是核心功能,對應命名是amplify.core。

2015728151503102.jpg (342×85)

amplify.core是發布/訂閱設計模式的一個簡潔的、清晰的實現,加上注釋一共100多行。讀完amplify的源碼,就可以比較好地理解如何去實現一個發布/訂閱的設計模式。
代碼全貌

amplify.core的源碼整體結構如下:

(function( global, undefined ) {

var slice = [].slice,
  subscriptions = {};

var amplify = global.amplify = {
  publish: function( topic ) {
    // ...
  },

  subscribe: function( topic, context, callback, priority ) {
    // ...
  },

  unsubscribe: function( topic, context, callback ) {
    // ...
  }
};

}( this ) );

可以看到,amplify定義了一個名為amplify的全局變量(作為global的屬性),它有3個方法publish、subscribe、unsubscribe。此外,subscriptions作為一個局部變量,它將保存發布/訂閱模式涉及的所有自定義事件名及其關聯函數。
publish

publish即發布,它要求指定一個topic,也就是自定義事件名(或者就叫做話題),調用後,所有關聯到某個topic的函數,都將被依次調用:

publish: function( topic ) {
  // [1]
  if ( typeof topic !== "string" ) {
    throw new Error( "You must provide a valid topic to publish." );
  }
  // [2]
  var args = slice.call( arguments, 1 ),
    topicSubscriptions,
    subscription,
    length,
    i = 0,
    ret;

  if ( !subscriptions[ topic ] ) {
    return true;
  }
  // [3]
  topicSubscriptions = subscriptions[ topic ].slice();
  for ( length = topicSubscriptions.length; i < length; i++ ) {
    subscription = topicSubscriptions[ i ];
    ret = subscription.callback.apply( subscription.context, args );
    if ( ret === false ) {
      break;
    }
  }
  return ret !== false;
},

[1],參數topic必須要求是字符串,否則拋出一個錯誤。

[2],args將取得除topic之外的其他所有傳遞給publish函數的參數,並以數組形式保存。如果對應topic在subscriptions中沒有找到,則直接返回。

[3],topicSubscriptions作為一個數組,取得某一個topic下的所有關聯元素,其中每一個元素都包括callback及context兩部分。然後,遍歷元素,調用每一個關聯元素的callback,同時帶入元素的context和前面的額外參數args。如果任意一個關聯元素的回調函數返回false,則停止運行其他的並返回false。
subscribe

訂閱,如這個詞自己的含義那樣(就像訂本雜志什麼的),是建立topic和callback的關聯的步驟。比較特別的是,amplify在這裡還加入了priority(優先級)的概念,優先級的值越小,優先級越高,默認是10。優先級高的callback,將會在publish的時候,被先調用。這個順序的原理可以從前面的publish的源碼中看到,其實就是預先按照優先級從高到低依次排列好了某一topic的所有關聯元素。

subscribe: function( topic, context, callback, priority ) {
    if ( typeof topic !== "string" ) {
      throw new Error( "You must provide a valid topic to create a subscription." );
    }
    // [1]
    if ( arguments.length === 3 && typeof callback === "number" ) {
      priority = callback;
      callback = context;
      context = null;
    }
    if ( arguments.length === 2 ) {
      callback = context;
      context = null;
    }
    priority = priority || 10;
    // [2]
    var topicIndex = 0,
      topics = topic.split( /\s/ ),
      topicLength = topics.length,
      added;
    for ( ; topicIndex < topicLength; topicIndex++ ) {
      topic = topics[ topicIndex ];
      added = false;
      if ( !subscriptions[ topic ] ) {
        subscriptions[ topic ] = [];
      }
      // [3]
      var i = subscriptions[ topic ].length - 1,
        subscriptionInfo = {
          callback: callback,
          context: context,
          priority: priority
        };
      // [4]
      for ( ; i >= 0; i-- ) {
        if ( subscriptions[ topic ][ i ].priority <= priority ) {
          subscriptions[ topic ].splice( i + 1, 0, subscriptionInfo );
          added = true;
          break;
        }
      }
      // [5]
      if ( !added ) {
        subscriptions[ topic ].unshift( subscriptionInfo );
      }
    }

    return callback;
  },

[1],要理解這一部分,請看amplify提供的API示意:

amplify.subscribe( string topic, function callback )
amplify.subscribe( string topic, object context, function callback )
amplify.subscribe( string topic, function callback, number priority )
amplify.subscribe(
  string topic, object context, function callback, number priority )

可以看到,amplify允許多種參數形式,而當參數數目和類型不同的時候,位於特定位置的參數可能會被當做不同的內容。這也在其他很多JavaScript庫中可以見到。像這樣,通過參數數目和類型的判斷,就可以做到這種多參數形式的設計。

[2],訂閱的時候,topic是允許空格的,空白符將被當做分隔符,認為是將一個callback關聯到多個topic上,所以會使用一個循環。added用作標識符,表明新加入的這個元素是否已經添加到數組內,初始為false。

[3],每一個callback的保存,實際是一個對象,除callback外還帶上了context(默認為null)和priority。

[4],這個循環是在根據priority的值,找到關聯元素應處的位置。任何topic的關聯元素都是從無到有,且依照priority數值從小到大排列(已排序的)。因此,在比較的時候,是先假設新加入的元素的priority數值較大(優先級低),從數組尾端向前比較,只要原數組中有關聯元素的priority數值比新加入元素的小,循環就可以中斷,且可以確定地用數組的splice方法將新加入的元素添加在此。如果循環一直運行到完畢,則可以確定新加入的元素的priority數值是最小的,此時added將保持為初始值false。

[5],如果到這個位置,元素還沒有被添加,那麼執行添加,切可以確定元素應該位於數組的最前面(或者是第一個元素)。
unsubscribe

雖然發布和訂閱是最主要的,但也會有需要退訂的時候(雜志不想看了果斷退!)。所以,還會需要一個unsubscribe。

unsubscribe: function( topic, context, callback ) {
  if ( typeof topic !== "string" ) {
    throw new Error( "You must provide a valid topic to remove a subscription." );
  }

  if ( arguments.length === 2 ) {
    callback = context;
    context = null;
  }

  if ( !subscriptions[ topic ] ) {
    return;
  }

  var length = subscriptions[ topic ].length,
    i = 0;

  for ( ; i < length; i++ ) {
    if ( subscriptions[ topic ][ i ].callback === callback ) {
      if ( !context || subscriptions[ topic ][ i ].context === context ) {
        subscriptions[ topic ].splice( i, 1 );
        
        // Adjust counter and length for removed item
        i--;
        length--;
      }
    }
  }
}

讀過前面的源碼後,這部分看起來就很容易理解了。根據指定的topic遍歷關聯元素,找到callback一致的,然後刪除它。由於使用的是splice方法,會直接修改原始數組,因此需要手工對i和length再做一次調整。
Amplify使用示例

官方提供的其中一個使用示例是:

amplify.subscribe( "dataexample", function( data ) {
  alert( data.foo ); // bar
});

//...

amplify.publish( "dataexample", { foo: "bar" } );

結合前面的源碼部分,是否對發布/訂閱這一設計模式有了更明確的體會呢?
補充說明

你可能也注意到了,AmplifyJS所實現的典型的發布/訂閱是同步的(synchronous)。也就是說,在運行amplify.publish(topic)的時候,是會沒有任何延遲地把某一個topic附帶的所有回調,全部都運行一遍。
結語

Pub/Sub是一個比較容易理解的設計模式,但非常有用,可以應對大型應用的復雜邏輯。本文簡析的AmplifyJS是我覺得寫得比較有章法而且簡明切題(針對單一功能)的JavaScript庫,所以在此分享給大家。

XML學習教程| jQuery入門知識| AJAX入門| Dreamweaver教程| Fireworks入門知識| SEO技巧| SEO優化集錦|
Copyright © DIV+CSS佈局教程網 All Rights Reserved