使用AmplifyJS组件配合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。

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

(0)

相关推荐

  • Node.js编写组件的三种实现方式

    首先介绍使用v8 API跟使用swig框架的不同: (1)v8 API方式为官方提供的原生方法,功能强大而完善,缺点是需要熟悉v8 API,编写起来比较麻烦,是js强相关的,不容易支持其它脚本语言. (2)swig为第三方支持,一个强大的组件开发工具,支持为python.lua.js等多种常见脚本语言生成C++组件包装代码,swig使用者只需要编写C++代码和swig配置文件即可开发各种脚本语言的C++组件,不需要了解各种脚本语言的组件开发框架,缺点是不支持javascript的回调,文档和de

  • JS组件中bootstrap multiselect两大组件较量

    两个这种组件,大体样式和功能基本相同,本文就来带领大家看看这两个组件的用法. 一.组件说明以及API 1.第一个组件--multiple-select.这个组件风格简单.文档全.功能强大.但是觉得它选中的效果不太好.关于它的效果展示,我们放在后面. 2.第二个组件--bootstrap-multiselect.这个组件风格和第一个非常相似,文档也挺全面. 二.Multiple-select组件 1.组件说明 这个组件需要的浏览器支持如下: IE 7+ Chrome 8+ Firefox 10+

  • javascript表格的渲染组件

    表格的渲染组件,demo请点击http://lovewebgames.com/jsmodule/table.html,git源码请点击https://github.com/tianxiangbing/table 如上图所示,功能基本包括常用表格中遇到的分页.搜索.删除.AJAX操作.由于是用的HANDLEBARS渲染的,所以样式可能很好的控制,要加新的功能也较容易. 调用例子 html <div class="form"> 名称: <input type="

  • JS组件系列之Bootstrap Icon图标选择组件

    前言:最近好多朋友在群里面聊到bootstrap icon图标的问题,比如最常见的菜单管理,每个菜单肯定需要一个对应的菜单图标,要是有一个可视化的图标选择组件就好了,最好是直接选择图标,就能得到对应的class样式.于是乎各种百度,皇天不负有心人,最后被博主找到了,感觉效果还不错,并且支持自定义的图标,今天就拿出来分享下,绝对的干货哦! 一.Bootstrap icon picker组件 这个组件是在github上面搜索的时候找到的,初初看上去,确实是很不错的,并且是基于bootstrap风格的

  • JavaScript组件开发完整示例

    本文实例讲述了JavaScript组件开发的技巧.分享给大家供大家参考,具体如下: 使用JavaScript,按照面向对象的思想来构建组件. 现以构建一个TAB组件为例. 从功能上讲,组件包括可视部分和逻辑控制部分:从代码结构上讲,组件包括代码部分和资源部分(样式.图片等). 组件的特点:高内聚,低耦合(不与其他代码逻辑交叉,可以继承,包含):封装性(隐藏私有方法和变量):可重用性(可反复多次使用,用来组装更复杂的应用). <html> <head> <meta http-e

  • JavaScript资源预加载组件和滑屏组件的使用推荐

    资源预加载组件--preload 队列,可以支持队列加载和回调,也可以加载视频或者音频 进度条,可以动态获取进度条信息 支持img标签的预加载,添加pSrc属性即可 原生ES5 demo Install: git clone https://github.com/jayZOU/preload.git npm install npm run es6 访问http://localhost:8080/es6-demo Examples <audio pSrc="../public/audio/a

  • 不用一句js代码初始化组件

    最近使用bootstrap组件的时候发现一个易用性问题,很多简单的组件初始化都需要在JS里面写很多的初始化代码,比如一个简单的select标签,因为仅仅只是需要从后台获取数据填充到option里面,可是从后台取数据就需要js的初始化,所以导致页面初始化的时候js的初始化代码里面出现很多重复的代码,看着很闹心.于是想起bootstrap table里面的data属性来,如果能够直接在html里面使用data-*这种方式来初始化简单的组件,那该多爽.我们先来看看bootstrap table的文档:

  • JS拖拽组件学习使用

    JS代码需要常写,不然容易生疏,最近虽然一直在看JS的原型,行为委托等知识点,但是动手写代码的量略有减少.本文与大家分享一个拖拽组件,供大家参考,具体内容如下 首先,看一下拖拽的原理. 被拖拽元素位置的变化,left值的变化其实就是鼠标位置水平方向的变化值,e.clientX - 鼠标左键按下时e.clientX. top值的变化其实就是鼠标位置竖直方向的变化值,e.clientY - 鼠标左键按下时e.clientY. 另外就是设置拖拽的范围,上下左右不得超过父元素所在的区域. functio

  • JS组件Form表单验证神器BootstrapValidator

    本文为大家分享了JS组件Form表单验证神器BootstrapValidator,供大家参考,具体内容如下 1.初级用法 来看bootstrapvalidator的描述:A jQuery form validator for Bootstrap 3.从描述中我们就可以知道它至少需要jQuery.bootstrap的支持.我们首先引入需要的js组件: <script src="~/Scripts/jquery-1.10.2.js"></script> <sc

  • 封装属于自己的JS组件

    一.扩展已经存在的组件 1.需求背景 很多时候,我们使用jquery.ajax的方式向后台发送请求,型如 $.ajax({ type: "post", url: "/User/Edit", data: { data: JSON.stringify(postdata) }, success: function (data, status) { if (status == "success") { toastr.success('提交数据成功');

随机推荐