首頁  >  文章  >  web前端  >  使用AmplifyJS元件配合JavaScript進行程式設計的指南_基礎知識

使用AmplifyJS元件配合JavaScript進行程式設計的指南_基礎知識

WBOY
WBOY原創
2016-05-16 15:48:24942瀏覽

事件分送的作用

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

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库,所以在此分享给大家。

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn