本文深入解析AmplifyJS库的核心功能,重点剖析其发布/订阅(Pub/Sub)设计模式的实现原理。通过解读amplify.core源码,详细讲解publish、subscribe及unsubscribe方法的逻辑,包括优先级处理、多参数兼容及同步执行机制。文章展示了如何利用AmplifyJS解耦浏览器原生事件与业务逻辑,提升代码可维护性,并提供实际使用示例。
事件分发的作用
事件分发的作用
在构建页面交互功能时,最直观的做法是为页面元素绑定事件,并在处理函数中执行相应操作。例如以下代码:
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只是一个随意定义的自定义事件名称。
显然流程尚未结束,为实现前述复杂功能,还需将自定义事件与具体操作关联:
amplify.subscribe( "aya:clicked", doMission1);
// ...
amplify.subscribe( "aya:clicked", doMission2);
// ...
这看似绕回原点,实则有益。一方面,浏览器原生事件的监听被分离并固化,后续若逻辑变更(如减少功能),只需调整自定义事件关联代码,无需再处理原生事件。另一方面,逻辑调整更灵活,可在任意代码位置通过subscribe添加功能,并按自定义事件名进行分类管理。
简言之,事件分发通过引入一层自定义事件(在简单逻辑下可能显得冗余),降低了模块间耦合度,使逻辑更清晰有序,利于后续维护。
AmplifyJS源码解析
等等,前文频繁出现的amplify究竟是什么?
好了,现在正式介绍它。 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属性),包含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函数的所有参数,并保存为数组。若subscriptions中未找到对应topic,则直接返回。
[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--;
}
}
}
}
Amplify使用示例
阅读前述源码后,这部分易于理解。根据指定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库,故在此分享。