Cet article présente une astuce jquery qui permet à n'importe quel objet composant de prendre en charge la gestion d'événements de type DOM, c'est-à-dire qu'en plus de distribuer des événements, d'ajouter ou de supprimer des écouteurs d'événements, il peut également prendre en charge la propagation d'événements et empêcher les comportements par défaut des événements. . etc. Avec l'aide de jquery, utiliser cette méthode pour gérer les événements d'objets ordinaires revient exactement à gérer les événements d'objets DOM, même si au final, lorsque vous voyez le contenu spécifique de cette petite astuce, vous pouvez avoir l'impression que c'est le cas ou non. c'est le cas, mais je pense que si l'implémentation du modèle de publication-abonnement ordinaire peut être remplacée par un mécanisme d'événement de type DOM, les composants développés auront certainement une plus grande flexibilité et évolutivité, et c'est aussi la première fois que j'utilise cette méthode (insight Parce qu'elle est trop superficielle), je pense que sa valeur est assez grande, alors je l'ai partagée.
Avant de présenter formellement cette technique, je dois d'abord parler d'une méthode que j'ai envisagée auparavant, qui est le modèle de publication-abonnement, pour voir quels problèmes elle peut résoudre et ses problèmes existants.
1. Modèle de publication-abonnement
De nombreux blogs, y compris des livres, disent que si JavaScript souhaite implémenter des événements personnalisés pour les composants, il peut utiliser le modèle de publication-abonnement. Au début, je le croyais fermement, alors j'en ai écrit un en utilisant $.Callbacks de jquery :
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; });
Tant qu'un composant hérite de cette EventBase, il peut hériter de la méthode de déclenchement on off qu'il fournit pour compléter les fonctions d'abonnement, de publication et de désabonnement des messages, telles que le FileUploadBaseView que je souhaite implémenter ci-dessous :
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; });
Le test d'appel réel est le suivant :
Dans le test, un objet FileUploadBaseView f a été instancié, son attribut name a été défini, un écouteur lié à hello a été ajouté via la méthode on, et enfin l'écouteur hello a été déclenché via la méthode trigger, et supplémentaire avec deux paramètres, à l'intérieur de l'écouteur, en plus d'accéder aux données transmises par le déclencheur via les paramètres de fonction de l'écouteur, l'objet f est également accessible via celui-ci.
D'après les résultats actuels, cette méthode semble bonne, mais j'ai rencontré un problème lorsque je voulais continuer à implémenter FileUploadBaseView. Regardez les options liées à l'abonnement dont j'avais lors de la conception de ce composant :
Ma conception originale est la suivante : ces abonnements sont définis par paires, et une paire d'abonnements correspond à une certaine méthode d'instance. Par exemple, l'abonnement avec before sera déclenché avant que la méthode d'instance correspondante (render) ne soit appelée, et le l'abonnement sans before sera déclenché avant l'appel de la méthode d'instance correspondante (render). Cet abonnement sera déclenché après l'appel de la méthode d'instance correspondante (render), et il est également requis que si l'abonnement avec before renvoie false, l'instance correspondante méthode et les souscriptions ultérieures ne seront pas exécutées. La dernière exigence de conception est de considérer qu'avant d'appeler la méthode d'instance du composant, l'appel à la méthode d'instance actuelle peut devoir être annulé pour des raisons particulières. Par exemple, lorsque la méthode de suppression est appelée, certaines données ne peuvent pas être supprimées. , afin qu'il puisse être souscrit avant. Faites une vérification, renvoyez true s'il peut être supprimé, renvoyez false s'il ne peut pas être supprimé, puis ajoutez un jugement après avoir déclenché l'abonnement avant dans la méthode d'instance, similaire à l'approche suivante :
Mais cette approche ne peut être implémentée qu'en mode fonction de rappel pur. Elle ne fonctionne pas en mode publication-abonnement, car la fonction de rappel ne sera liée qu'à une seule référence de fonction, et en mode publication-abonnement, la le même message peut Il y a plusieurs abonnements. Si cette approche est appliquée à la publication-abonnement, lorsque this.trigger('beforeRender') est appelé, tous les abonnements associés à beforeRender seront appelés une fois. Alors la valeur de retour de quel abonnement est Exacte ? Peut-être direz-vous que la valeur de retour du dernier abonnement dans la file d'attente peut être utilisée comme critère. Dans la plupart des cas, cela peut être acceptable, mais lorsque nous ajoutons la logique consistant à "prendre la valeur de retour du dernier abonnement de la file d'attente comme valeur". critère de jugement" à Lors de l'utilisation d'EventBase, il y aura un gros risque, c'est-à-dire que lors de son utilisation en externe, l'ordre des abonnements doit être clairement géré et les abonnements liés à une logique particulière telle que la vérification doivent être placés à la fin. , et ce type de méthode de développement qui n'a rien à voir avec la grammaire ou la compilation et qui a des exigences en matière d'ordre de codage entraînera des risques de sécurité relativement importants pour le logiciel. Qui peut garantir que l'ordre des abonnements peut être contrôlé à tout moment et dans n'importe quel scénario, encore moins dans l'entreprise. Il se peut que certains nouveaux arrivants ne sachent pas qu'il existe de telles restrictions sur ce que vous écrivez.
Le moyen idéal pour résoudre ce problème est d'encapsuler le message dans un objet, qui sera transmis à Parmi tous ses abonnements, dont on pense que la logique après la publication de ce message devrait être bloquée, il suffit d'appeler la méthode PreventDefault() de ce message, puis après avoir publié le message en externe, appelez la méthode isDefaultPrevented() du message pour juger :
Cette approche est la même que l'utilisation de jquery pour gérer les événements des objets DOM. Par exemple, la plupart des composants de bootstrap et les composants que j'ai écrits dans certains blogs précédents utilisent cette méthode pour ajouter une logique de jugement supplémentaire, telle que le composant d'alerte. a ce jugement lorsque la méthode close est exécutée :
按照这个思路去改造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(); });
Dans ce test, j'ai défini un composant Demo qui n'a rien à voir avec le DOM et j'ai hérité de la classe de gestion d'événements EventBase. J'ai ajouté 3 auditeurs à l'événement beforeRender, dont un a un appel à PredictDefault(), et le rappel. Ce n'est pas le dernier, le résultat final du test est :
Comme le montrent les résultats, le code logique principal de la méthode de rendu et l'événement de rendu ultérieur ne sont pas exécutés. Tous les écouteurs beforeRender sont exécutés, indiquant que e.preventDefault() prend effet et n'affecte pas le File d’attente d’événements beforeRender. Ayez un impact.
Ainsi, à en juger par les deux tests, grâce à EventBase modifié, nous avons obtenu une méthode qui permet à n'importe quel objet de prendre en charge le mécanisme de gestion d'événements jquery. À l'avenir, lorsque nous envisagerons d'utiliser le mécanisme d'événements pour le découplage, nous n'aurons plus besoin de le faire. réfléchissez-y. Le modèle de publication-abonnement a été introduit en premier, et cette méthode est relativement plus puissante et stable, et est plus conforme à votre habitude habituelle d'utiliser jquery pour faire fonctionner le DOM.
4. Résumé de cet article
Il y a deux points qui doivent être expliqués :
1) Même si vous n'utilisez pas jquery, vous pouvez suivre l'idée proposée à la fin de la partie 1 et transformer le modèle de publication-abonnement habituel en partie 1. C'est juste qu'utiliser jquery est plus concis
2) Enfin, le mécanisme d'événements de jquery est utilisé pour réaliser la gestion des événements de n'importe quel objet. D'une part, le mode proxy est utilisé, et plus important encore, le mode publication-abonnement est utilisé, mais l'implémentation finale est effectuée par. jquery pour nous. La première partie de l'implémentation de publication-abonnement a été transformée.
Le contenu ci-dessus est lié aux techniques jQuery permettant à n'importe quel composant de prendre en charge la gestion d'événements de type DOM. J'espère qu'il sera utile à tout le monde !