Heim  >  Artikel  >  Web-Frontend  >  jQuery-Tipps, um jeder Komponente die Unterstützung von DOM-ähnlichem Event Management_jquery zu ermöglichen

jQuery-Tipps, um jeder Komponente die Unterstützung von DOM-ähnlichem Event Management_jquery zu ermöglichen

WBOY
WBOYOriginal
2016-05-16 15:06:381250Durchsuche

In diesem Artikel wird ein JQuery-Tipp vorgestellt, der es jedem Komponentenobjekt ermöglicht, eine DOM-ähnliche Ereignisverwaltung zu unterstützen. Das heißt, neben dem Versenden von Ereignissen, dem Hinzufügen oder Löschen von Ereignis-Listenern kann es auch das Ereignis-Bubbling unterstützen und Ereignis-Standardverhalten verhindern . usw. Mit Hilfe von jquery ist die Verwendung dieser Methode zum Verwalten von Ereignissen gewöhnlicher Objekte genau das Gleiche wie das Verwalten von Ereignissen von DOM-Objekten. Wenn Sie jedoch den spezifischen Inhalt dieses kleinen Tricks sehen, haben Sie möglicherweise das Gefühl, dass dies der Fall ist oder nicht Das ist der Fall, aber ich bin der Meinung, dass die entwickelten Komponenten definitiv flexibler und skalierbarer sein werden, wenn die Implementierung des gewöhnlichen Publish-Subscribe-Modells in einen DOM-ähnlichen Ereignismechanismus geändert werden kann, und es ist auch das erste Mal, dass ich sie verwende Diese Methode (Einsicht) Da sie zu oberflächlich ist, halte ich ihren Wert für ziemlich groß, deshalb habe ich sie geteilt.

Bevor ich diese Technik offiziell vorstelle, muss ich zunächst über eine Methode sprechen, die ich zuvor in Betracht gezogen habe, nämlich das Publish-Subscribe-Modell, um zu sehen, welche Probleme es lösen kann und welche bestehenden Probleme es gibt.

1. Publish-Subscribe-Modell

Viele Blogs, darunter auch Bücher, sagen, dass JavaScript, wenn es benutzerdefinierte Ereignisse für Komponenten implementieren möchte, das Publish-Subscribe-Modell verwenden kann. Zuerst war ich fest davon überzeugt, also habe ich eines mit $.Callbacks von jquery geschrieben:

define(function(require, exports, module) {
var $ = require('jquery');
var Class = require('./class');
function isFunc(f) {
return Object.prototype.toString.apply(f) === '[object Function]';
}
/**
* 这个基类可以让普通的类具备事件驱动的能力
* 提供类似jq的on off trigger方法,不考虑one方法,也不考虑命名空间
* 举例:
* var e = new EventBase();
* e.on('load', function(){
* console.log('loaded');
* });
* e.trigger('load');//loaded
* e.off('load');
*/
var EventBase = Class({
instanceMembers: {
init: function () {
this.events = {};
//把$.Callbacks的flag设置成一个实例属性,以便子类可以覆盖
this.CALLBACKS_FLAG = 'unique';
},
on: function (type, callback) {
type = $.trim(type);
//如果type或者callback参数无效则不处理
if (!(type && isFunc(callback))) return;
var event = this.events[type];
if (!event) {
//定义一个新的jq队列,且该队列不能添加重复的回调
event = this.events[type] = $.Callbacks(this.CALLBACKS_FLAG);
}
//把callback添加到这个队列中,这个队列可以通过type来访问
event.add(callback);
},
off: function (type, callback) {
type = $.trim(type);
if (!type) return;
var event = this.events[type];
if (!event) return;
if (isFunc(callback)) {
//如果同时传递type跟callback,则将callback从type对应的队列中移除
event.remove(callback);
} else {
//否则就移除整个type对应的队列
delete this.events[type];
}
},
trigger: function () {
var args = [].slice.apply(arguments),
type = args[0];//第一个参数转为type
type = $.trim(type);
if (!type) return;
var event = this.events[type];
if (!event) return;
//用剩下的参数来触发type对应的回调
//同时把回调的上下文设置成当前实例
event.fireWith(this, args.slice(1));
}
}
});
return EventBase;
});

(Basierend auf Seajs und der Vererbungsbibliothek class.js, eingeführt in „Detaillierte Erläuterung der Javascript-Vererbungsimplementierung“)

Solange eine Komponente diese EventBase erbt, kann sie die Ein-Aus-Trigger-Methode erben, die sie bereitstellt, um die Funktionen zum Abonnieren, Veröffentlichen und Abmelden von Nachrichten abzuschließen, wie zum Beispiel die FileUploadBaseView, die ich unten implementieren möchte:

define(function(require, exports, module) {
var $ = require('jquery');
var Class = require('./class');
var EventBase = require('./eventBase');
var DEFAULTS = {
data: [], //要展示的数据列表,列表元素必须是object类型的,如[{url: 'xxx.png'},{url: 'yyyy.png'}]
sizeLimit: 0, //用来限制BaseView中的展示的元素个数,为0表示不限制
readonly: false, //用来控制BaseView中的元素是否允许增加和删除
onBeforeRender: $.noop, //对应beforeRender事件,在render方法调用前触发
onRender: $.noop, //对应render事件,在render方法调用后触发
onBeforeAppend: $.noop, //对应beforeAppend事件,在append方法调用前触发
onAppend: $.noop, //对应append事件,在append方法调用后触发
onBeforeRemove: $.noop, //对应beforeRemove事件,在remove方法调用前触发
onRemove: $.noop //对应remove事件,在remove方法调用后触发
};
/**
* 数据解析,给每个元素的添加一个唯一标识_uuid,方便查找
*/
function resolveData(ctx, data){
var time = new Date().getTime();
return $.map(data, function(d){
d._uuid = '_uuid' + time + Math.floor(Math.random() * 100000);
});
}
var FileUploadBaseView = Class({
instanceMembers: {
init: function (options) {
this.base();
this.options = this.getOptions(options);
},
getOptions: function(options) {
return $.extend({}, DEFAULTS, options);
},
render: function(){
},
append: function(data){
},
remove: function(prop){
}
},
extend: EventBase
});
return FileUploadBaseView;
});

Der eigentliche Anruftest läuft wie folgt ab:



Im Test wurde ein FileUploadBaseView-Objekt f instanziiert und sein Namensattribut wurde über die on-Methode hinzugefügt. Schließlich wurde der Hello-Listener über die Trigger-Methode und zusätzliche Parameter ausgelöst. Innerhalb des Listeners kann nicht nur über die Funktionsparameter des Listeners auf die vom Trigger übergebenen Daten zugegriffen werden, sondern auch auf das f-Objekt.

Aus den aktuellen Ergebnissen sieht diese Methode gut aus, aber ich bin auf ein Problem gestoßen, als ich mit der Implementierung von FileUploadBaseView fortfahren wollte. Schauen Sie sich die abonnementbezogenen Optionen an, die ich beim Entwerfen dieser Komponente hatte:

Mein ursprünglicher Entwurf ist: Diese Abonnements werden paarweise definiert, und ein Abonnementpaar entspricht einer bestimmten Instanzmethode. Beispielsweise wird das Abonnement mit before ausgelöst, bevor die entsprechende Instanzmethode (Render) aufgerufen wird Ein Abonnement ohne „davor“ wird ausgelöst, bevor die entsprechende Instanzmethode (render) aufgerufen wird. Dieses Abonnement wird ausgelöst, nachdem die entsprechende Instanzmethode (render) aufgerufen wird. Außerdem ist es erforderlich, dass die entsprechende Instanz „false“ zurückgibt, wenn das Abonnement mit „davor“ zurückgegeben wird -Methode und nachfolgende Abonnements werden nicht ausgeführt. Die letzte Entwurfsanforderung besteht darin, zu berücksichtigen, dass vor dem Aufruf der Instanzmethode der Komponente der Aufruf der aktuellen Instanzmethode aus bestimmten Gründen möglicherweise abgebrochen werden muss. Wenn beispielsweise die Remove-Methode aufgerufen wird, können einige Daten nicht entfernt werden , dann kann es vorher abonniert werden. Führen Sie eine Überprüfung durch, geben Sie true zurück, wenn es gelöscht werden kann, geben Sie false zurück, wenn es nicht gelöscht werden kann, und fügen Sie dann ein Urteil hinzu, nachdem Sie das Vorab-Abonnement in der Instanzmethode ausgelöst haben, ähnlich dem folgenden Ansatz:

Dieser Ansatz kann jedoch nur im reinen Callback-Funktionsmodus implementiert werden. Er funktioniert nicht im Publish-Subscribe-Modus, da sich die Callback-Funktion nur auf eine Funktionsreferenz bezieht, und im Publish-Subscribe-Modus Dieselbe Nachricht kann vorhanden sein. Wenn dieser Ansatz auf Publish-Subscribe angewendet wird, werden beim Aufruf von this.trigger('beforeRender') alle mit beforeRender verknüpften Abonnements einmal aufgerufen. Der Rückgabewert welches Abonnements ist dann genau. Vielleicht werden Sie sagen, dass der Rückgabewert des letzten Abonnements in der Warteschlange als Kriterium verwendet werden kann. In den meisten Fällen mag dies in Ordnung sein, aber wenn wir die Logik hinzufügen, „den Rückgabewert des letzten Abonnements in der Warteschlange zu verwenden“. „Beurteilungskriterium“ bei der Verwendung von EventBase besteht ein großes Risiko, d. Und diese Art von Entwicklungsmethode, die nichts mit Grammatik oder Kompilierung zu tun hat und Anforderungen an die Codierungsreihenfolge stellt, birgt relativ große Sicherheitsrisiken für die Software. Wer kann garantieren, dass die Reihenfolge der Abonnements jederzeit und in jedem Szenario kontrolliert werden kann? geschweige denn im Unternehmen. Es kann sein, dass es einige Neueinsteiger gibt, die nicht wissen, dass es beim Schreiben solche Einschränkungen gibt.

Der perfekte Weg, dieses Problem zu lösen, besteht darin, die Nachricht in ein Objekt zu kapseln, das an alle seine Abonnements übergeben wird, von denen man denkt, dass die Logik nach der Veröffentlichung dieser Nachricht blockiert werden sollte. Rufen Sie einfach die Methode „preventDefault()“ auf Rufen Sie nach der externen Veröffentlichung der Nachricht die Methode isDefaultPrevented() der Nachricht auf, um zu beurteilen:

Dieser Ansatz ist derselbe wie die Verwendung von jquery zum Verwalten von Ereignissen von DOM-Objekten. Beispielsweise verwenden die meisten Komponenten von Bootstrap und die Komponenten, die ich in einigen früheren Blogs geschrieben habe, diese Methode, um zusätzliche Beurteilungslogik hinzuzufügen, z. B. die Alarmkomponente hat dieses Urteil, wenn die Methode close ausgeführt wird:

按照这个思路去改造EventBase是一个解决问题的方法,但是jquery的一个小技巧,能够让我们把整个普通对象的事件管理变得更加简单,下面就让我们来瞧一瞧它的庐山真面目。

2. jquery小技巧模式

1)技巧一

如果在定义组件的时候,这个组件是跟DOM对象有关联的,比如下面这种形式:

那么我们可以完全给这个组件添加on off trigger one这几个常用事件管理的方法,然后将这些方法代理到$element的相应方法上:

通过代理,当调用组件的on方法时,其实调用的是$element的on方法,这样的话这种类型的组件就能支持完美的事件管理了。

2)技巧二

第一个技巧只能适用于跟DOM有关联的组件,对于那些跟DOM完全没有关联的组件该怎么添加像前面这样完美的事件管理机制呢?其实方法也很简单,只是我自己以前真的是没这么用过,所以这一次用起来才会觉得特别新鲜:

看截图中框起来的部分,只要给jquery的构造函数传递一个空对象,它就会返回一个完美支持事件管理的jquery对象。而且除了事件管理的功能外,由于它是一个jquery对象。所以jquery原型上的所有方法它都能调用,将来要是需要借用jquery其它的跟DOM无关的方法,说不定也能参考这个小技巧来实现。

3. 完美的事件管理实现

考虑到第2部分介绍的2种方式里面有重复的逻辑代码,如果把它们结合起来的话,就可以适用所有的开发组件的场景,也就能达到本文标题和开篇提到的让任意对象支持事件管理功能的目标了,所以最后结合前面两个技巧,把EventBase改造如下(是不是够简单):

define(function(require, exports, module) {
var $ = require('jquery');
var Class = require('./class');
/**
* 这个基类可以让普通的类具备jquery对象的事件管理能力
*/
var EventBase = Class({
instanceMembers: {
init: function (_jqObject) {
this._jqObject = _jqObject && _jqObject instanceof $ && _jqObject || $({});
},
on: function(){
return $.fn.on.apply(this._jqObject, arguments);
},
one: function(){
return $.fn.one.apply(this._jqObject, arguments);
},
off: function(){
return $.fn.off.apply(this._jqObject, arguments);
},
trigger: function(){
return $.fn.trigger.apply(this._jqObject, arguments);
}
}
});
return EventBase;
});

实际调用测试如下

1)模拟跟DOM关联的组件

测试代码一:

define(function(require, exports, module) {
var $ = require('jquery');
var Class = require('mod/class');
var EventBase = require('mod/eventBase');
var Demo = window.demo = Class({
instanceMembers: {
init: function (element,options) {
this.$element = $(element);
this.base(this.$element);
//添加监听
this.on('beforeRender', $.proxy(options.onBeforeRender, this));
this.on('render', $.proxy(options.onRender, this));
},
render: function () {
//触发beforeRender事件
var e = $.Event('beforeRender');
this.trigger(e);
if(e.isDefaultPrevented())return;
//主要逻辑代码
console.log('render complete!');
//触发render事件
this.trigger('render');
}
},
extend: EventBase
});
var demo = new Demo('#demo', {
onBeforeRender: function(e) {
console.log('beforeRender event triggered!');
},
onRender: function(e) {
console.log('render event triggered!');
}
});
demo.render();
});

在这个测试里, 我定义了一个跟DOM关联的Demo组件并继承了EventBase这个事件管理的类,给beforeRender事件和render事件都添加了一个监听,render方法中也有打印信息来模拟真实的逻辑,实例化Demo的时候用到了#demo这个DOM元素,最后的测试结果是:

完全与预期一致。

测试代码二:

define(function(require, exports, module) {
var $ = require('jquery');
var Class = require('mod/class');
var EventBase = require('mod/eventBase');
var Demo = window.demo = Class({
instanceMembers: {
init: function (element,options) {
this.$element = $(element);
this.base(this.$element);
//添加监听
this.on('beforeRender', $.proxy(options.onBeforeRender, this));
this.on('render', $.proxy(options.onRender, this));
},
render: function () {
//触发beforeRender事件
var e = $.Event('beforeRender');
this.trigger(e);
if(e.isDefaultPrevented())return;
//主要逻辑代码
console.log('render complete!');
//触发render事件
this.trigger('render');
}
},
extend: EventBase
});
var demo = new Demo('#demo', {
onBeforeRender: function(e) {
console.log('beforeRender event triggered!');
},
onRender: function(e) {
console.log('render event triggered!');
}
});
demo.on('beforeRender', function(e) {
e.preventDefault();
console.log('beforeRender event triggered 2!');
});
demo.on('beforeRender', function(e) {
console.log('beforeRender event triggered 3!');
});
demo.render();
});

在这个测试了, 我定义了一个跟DOM相关的Demo组件并继承了EventBase这个事件管理的类,给beforeRender事件添加了3个监听,其中一个有加prevetDefault()的调用,而且该回调还不是最后一个,最后的测试结果是:

从结果可以看到,render方法的主要逻辑代码跟后面的render事件都没有执行,所有beforeRender的监听器都执行了,说明e.preventDefault()生效了,而且它没有对beforeRender的事件队列产生影响。

2)模拟跟DOM无关联的普通对象

测试代码一:

define(function(require, exports, module) {
var $ = require('jquery');
var Class = require('mod/class');
var EventBase = require('mod/eventBase');
var Demo = window.demo = Class({
instanceMembers: {
init: function (options) {
this.base();
//添加监听
this.on('beforeRender', $.proxy(options.onBeforeRender, this));
this.on('render', $.proxy(options.onRender, this));
},
render: function () {
//触发beforeRender事件
var e = $.Event('beforeRender');
this.trigger(e);
if(e.isDefaultPrevented())return;
//主要逻辑代码
console.log('render complete!');
//触发render事件
this.trigger('render');
}
},
extend: EventBase
});
var demo = new Demo({
onBeforeRender: function(e) {
console.log('beforeRender event triggered!');
},
onRender: function(e) {
console.log('render event triggered!');
}
});
demo.render();
});

在这个测试里, 我定义了一个跟DOM无关的Demo组件并继承了EventBase这个事件管理的类,给beforeRender事件和render事件都添加了一个监听,render方法中也有打印信息来模拟真实的逻辑,最后的测试结果是:

完全与预期的一致。

测试代码二:

define(function(require, exports, module) {
var $ = require('jquery');
var Class = require('mod/class');
var EventBase = require('mod/eventBase');
var Demo = window.demo = Class({
instanceMembers: {
init: function (options) {
this.base();
//添加监听
this.on('beforeRender', $.proxy(options.onBeforeRender, this));
this.on('render', $.proxy(options.onRender, this));
},
render: function () {
//触发beforeRender事件
var e = $.Event('beforeRender');
this.trigger(e);
if(e.isDefaultPrevented())return;
//主要逻辑代码
console.log('render complete!');
//触发render事件
this.trigger('render');
}
},
extend: EventBase
});
var demo = new Demo({
onBeforeRender: function(e) {
console.log('beforeRender event triggered!');
},
onRender: function(e) {
console.log('render event triggered!');
}
});
demo.on('beforeRender', function(e) {
e.preventDefault();
console.log('beforeRender event triggered 2!');
});
demo.on('beforeRender', function(e) {
console.log('beforeRender event triggered 3!');
});
demo.render();
});

在这个测试了, 我定义了一个跟DOM无关的Demo组件并继承了EventBase这个事件管理的类,给beforeRender事件添加了3个监听,其中一个有加prevetDefault()的调用,而且该回调还不是最后一个,最后的测试结果是:

从结果可以看到,render方法的主要逻辑代码跟后面的render事件都没有执行,所有beforeRender的监听器都执行了,说明e.preventDefault()生效了,而且它没有对beforeRender的事件队列产生影响。

所以从2个测试来看,通过改造后的EventBase,我们得到了一个可以让任意对象支持jquery事件管理机制的方法,将来在考虑用事件机制来解耦的时候,就不用再去考虑前面第一个介绍的发布-订阅模式了,而且相对而言这个方法功能更强更稳定,也更符合你平常使用jquery操作DOM的习惯。

4. 本文小结

有2点需要再说明一下的是:

1)即使不用jquery按照第1部分最后提出的思路,把第一部分常规的发布-订阅模式改造一下也可以的,只不过用jquery更加简洁些;

2)最终用jquery 的事件机制来实现任意对象的事件管理,一方面是用到了代理模式,更重要的还是要用发布-订阅模式,只不过最后的这个实现是由jquery帮我们把第一部分的发布-订阅实现改造好了而已。

以上内容是针对jQuery技巧之让任何组件都支持类似DOM的事件管理的相关知识,希望对大家有所帮助!

Stellungnahme:
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn