使用AmplifyJS组件配合JavaScript进行编程的指南


Posted in Javascript onJuly 28, 2015

事件分发的作用

在为页面添加各类交互功能时,我们熟知的最简单的做法就是为页面元素绑定事件,然后在事件处理函数中,做我们想要做的动作。就像这样的代码:

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。

使用AmplifyJS组件配合JavaScript进行编程的指南

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

Javascript 相关文章推荐
开发 Internet Explorer 右键功能表(ContextMenu)
Jul 03 Javascript
js判断当前浏览器类型,判断IE浏览器方法
Jun 02 Javascript
JavaScript设计模式之装饰者模式介绍
Dec 28 Javascript
jquery获取input type=text中的值的各种方式(总结)
Dec 02 Javascript
jQuery实现的仿百度,仿谷歌搜索下拉框效果示例
Dec 30 Javascript
vue中的event bus非父子组件通信解析
Oct 27 Javascript
Vue.js中对css的操作(修改)具体方式详解
Oct 30 Javascript
Angular 中使用 FineReport不显示报表直接打印预览
Aug 21 Javascript
JS实现星星海特效
Dec 24 Javascript
Vue实现PC端靠边悬浮球的代码
May 09 Javascript
微信小程序国际化探索实现(附源码地址)
May 20 Javascript
Electron实现应用打包、自动升级过程解析
Jul 07 Javascript
JavaScript编程中的Promise使用大全
Jul 28 #Javascript
javascript+html5实现绘制圆环的方法
Jul 28 #Javascript
学习Bootstrap组件之下拉菜单
Jul 28 #Javascript
深入了解JavaScript中的Symbol的使用方法
Jul 28 #Javascript
深入理解JavaScript中的箭头函数
Jul 28 #Javascript
解析JavaScript的ES6版本中的解构赋值
Jul 28 #Javascript
深入学习JavaScript中的Rest参数和参数默认值
Jul 28 #Javascript
You might like
Apache, PHP在Windows 9x/NT下的安装与配置 (二)
2006/10/09 PHP
PHP Global变量定义当前页面的全局变量实现探讨
2013/06/05 PHP
PHP检测字符串是否为UTF8编码的常用方法
2014/11/21 PHP
php文件缓存类用法实例分析
2015/04/22 PHP
php session实现多级目录存放实现代码
2016/02/03 PHP
javascript break指定标签打破多层循环示例
2014/01/20 Javascript
用js将内容复制到剪贴板兼容浏览器
2014/03/18 Javascript
Javascript递归打印Document层次关系实例分析
2015/05/15 Javascript
JS实现动态增加和删除li标签行的实例代码
2016/10/16 Javascript
easyui datagrid 大数据加载效率慢,优化解决方法(推荐)
2016/11/09 Javascript
node.js调用Chrome浏览器打开链接地址的方法
2017/05/17 Javascript
详解vue.js之props传递参数
2017/12/12 Javascript
vue中实现先请求数据再渲染dom分享
2018/03/17 Javascript
用npm安装vue和vue-cli,并使用webpack创建项目的方法
2018/09/28 Javascript
Jquery属性的获取/设置及样式添加/删除操作技巧分析
2019/12/23 jQuery
跟老齐学Python之print详解
2014/09/28 Python
Python实现的下载8000首儿歌的代码分享
2014/11/21 Python
Python图像处理之gif动态图的解析与合成操作详解
2018/12/30 Python
Pytorch中实现只导入部分模型参数的方式
2020/01/02 Python
python3实现将json对象存入Redis以及数据的导入导出
2020/07/16 Python
Python 利用OpenCV给照片换底色的示例代码
2020/08/03 Python
详解Canvas事件绑定
2018/06/27 HTML / CSS
详解HTML5中的元素与元素
2015/08/17 HTML / CSS
美国电视购物:QVC
2017/02/06 全球购物
PHP面试题集
2016/12/18 面试题
下列程序在32位linux或unix中的结果是什么
2015/01/26 面试题
大学生暑期实践感言
2014/02/26 职场文书
心理学专业求职信
2014/06/16 职场文书
大学生学习新党章思想汇报
2014/10/25 职场文书
发展党员工作情况汇报
2014/10/28 职场文书
小浪底导游词
2015/02/12 职场文书
党员自我评价2015
2015/03/03 职场文书
2015年党员岗位承诺书
2015/04/27 职场文书
单位病假条范文
2015/08/17 职场文书
python 如何执行控制台命令与操作剪切板
2021/05/20 Python
pytorch实现加载保存查看checkpoint文件
2022/07/15 Python