Rumah > Artikel > hujung hadapan web > Petua jQuery untuk membolehkan mana-mana komponen menyokong pengurusan acara seperti DOM_jquery
Artikel ini memperkenalkan petua jquery yang membenarkan mana-mana objek komponen menyokong pengurusan acara seperti DOM iaitu, selain menghantar acara, menambah atau memadamkan pendengar acara, ia juga boleh menyokong acara menggelegak dan menghalang gelagat lalai acara. . Dengan bantuan jquery, menggunakan kaedah ini untuk mengurus peristiwa objek biasa adalah sama seperti menguruskan acara objek DOM Walaupun pada akhirnya apabila anda melihat kandungan khusus helah kecil ini, anda mungkin merasakan bahawa ia adalah atau tidak kes itu, tetapi saya merasakan bahawa jika pelaksanaan model penerbitan-langganan biasa boleh ditukar kepada mekanisme acara seperti DOM, komponen yang dibangunkan pasti akan mempunyai fleksibiliti dan skalabiliti yang lebih besar, dan ia juga merupakan kali pertama saya menggunakan kaedah ini (wawasan Kerana terlalu cetek), saya rasa nilainya agak hebat, jadi saya kongsikannya.
Sebelum saya memperkenalkan teknik ini secara rasmi, saya mesti bercakap tentang kaedah yang saya pertimbangkan sebelum ini, iaitu model publish-subscribe, untuk melihat masalah yang boleh diselesaikan dan masalah sedia ada.
1. Terbitkan-langgan model
Banyak blog, termasuk buku, mengatakan bahawa jika JavaScript ingin melaksanakan acara tersuai untuk komponen, ia boleh menggunakan model publish-subscribe Pada mulanya, saya sangat percaya begitu, jadi saya menulis satu menggunakan $.Callbacks 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; });
Selagi mana-mana komponen mewarisi EventBase ini, ia boleh mewarisi kaedah pencetus hidup mati yang disediakannya untuk melengkapkan fungsi langganan, penerbitan dan nyahlangganan mesej, seperti FileUploadBaseView yang ingin saya laksanakan di bawah:
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; });
Ujian panggilan sebenar adalah seperti berikut:
Dalam ujian, objek FileUploadBaseView f telah dibuat seketika, atribut namanya ditetapkan, pendengar yang berkaitan dengan hello telah ditambahkan melalui kaedah on, dan akhirnya pendengar hello telah dicetuskan melalui kaedah pencetus, dan tambahan Dengan dua parameter, di dalam pendengar, selain mengakses data yang diluluskan oleh pencetus melalui parameter fungsi pendengar, objek f juga boleh diakses melalui ini.
Daripada hasil semasa, kaedah ini kelihatan baik, tetapi saya menghadapi masalah apabila saya ingin terus melaksanakan FileUploadBaseView. Lihat pilihan berkaitan langganan yang saya ada semasa mereka bentuk komponen ini:
Reka bentuk asal saya ialah: langganan ini ditakrifkan secara berpasangan dan sepasang langganan sepadan dengan kaedah tika tertentu Contohnya, langganan dengan sebelum akan dicetuskan sebelum kaedah tika yang sepadan (render) dipanggil dan langganan tanpa sebelum akan dicetuskan sebelum kaedah contoh yang sepadan (render) dipanggil Langganan itu akan dicetuskan selepas kaedah contoh yang sepadan (render) dipanggil, dan juga diperlukan bahawa jika langganan dengan sebelum mengembalikan palsu, tika yang sepadan. kaedah dan langganan seterusnya tidak akan dilaksanakan. Keperluan reka bentuk terakhir adalah untuk mempertimbangkan bahawa sebelum memanggil kaedah contoh komponen, panggilan ke kaedah contoh semasa mungkin perlu dibatalkan kerana beberapa sebab khas, contohnya, apabila kaedah alih keluar dipanggil, sesetengah data tidak boleh dialih keluar , jadi ia boleh dilanggan sebelum Lakukan beberapa pengesahan, kembalikan benar jika ia boleh dipadamkan, kembalikan palsu jika ia tidak boleh dipadamkan, dan kemudian tambahkan penghakiman selepas mencetuskan langganan sebelum dalam kaedah contoh, serupa dengan pendekatan berikut:
Tetapi pendekatan ini hanya boleh dilaksanakan dalam mod fungsi panggil balik tulen Ia tidak berfungsi dalam mod publish-subscribe, kerana fungsi panggil balik hanya akan dikaitkan dengan satu rujukan fungsi, dan dalam mod publish-subscribe, the. mesej yang sama mungkin Terdapat berbilang langganan Jika pendekatan ini digunakan untuk publish-subscribe, apabila this.trigger('beforeRender') dipanggil, semua langganan yang dikaitkan dengan beforeRender akan dipanggil sekali. Mungkin anda akan mengatakan bahawa nilai pulangan langganan terakhir dalam baris gilir boleh digunakan sebagai kriteria Dalam kebanyakan kes, ini mungkin baik, tetapi apabila kita menambah logik "mengambil nilai pulangan langganan terakhir bagi baris gilir. kriteria penghakiman" kepada Apabila menggunakan EventBase, akan ada risiko yang besar, iaitu, apabila menggunakannya secara luaran, susunan langganan mesti diurus dengan jelas, dan langganan yang berkaitan dengan logik khas seperti pengesahan mesti diletakkan pada penghujungnya. , dan kaedah pembangunan jenis ini yang tidak ada kaitan dengan tatabahasa atau kompilasi dan mempunyai keperluan untuk susunan pengekodan akan membawa risiko keselamatan yang agak besar kepada perisian Siapa yang boleh menjamin bahawa susunan langganan boleh dikawal pada bila-bila masa dan dalam sebarang senario, apatah lagi dalam syarikat Mungkin ada beberapa pendatang baru yang tidak tahu bahawa terdapat sekatan sedemikian pada apa yang anda tulis.
Cara terbaik untuk menyelesaikan masalah ini ialah dengan merangkum mesej ke dalam objek, yang akan dihantar kepada Antara semua langganannya, yang mana satu berpendapat bahawa logik selepas menerbitkan mesej ini harus disekat, cuma panggil kaedah preventDefault() daripada mesej ini, dan kemudian selepas menerbitkan mesej secara luaran, panggil kaedah isDefaultPrevented() mesej untuk menilai:
Pendekatan ini sama seperti menggunakan jquery untuk mengurus acara objek DOM Contohnya, kebanyakan komponen bootstrap dan komponen yang saya tulis dalam beberapa blog sebelum ini menggunakan kaedah ini untuk menambah logik pertimbangan tambahan, seperti komponen amaran mempunyai penghakiman ini apabila kaedah tutup dilaksanakan:
按照这个思路去改造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(); });
In diesem Test habe ich eine Demo-Komponente definiert, die nichts mit DOM zu tun hat, und die Ereignisverwaltungsklasse EventBase geerbt. Ich habe dem beforeRender-Ereignis drei Listener hinzugefügt, von denen einer einen Aufruf von PredictDefault() und den Rückruf hat Es ist nicht das letzte, das endgültige Testergebnis lautet:
Wie aus den Ergebnissen hervorgeht, werden der Hauptlogikcode der Render-Methode und das nachfolgende Render-Ereignis nicht ausgeführt. Alle beforeRender-Listener werden ausgeführt, was darauf hinweist, dass e.preventDefault() wirksam wird und keine Auswirkungen auf die hat beforeRender-Ereigniswarteschlange.
Nach den beiden Tests zu urteilen, haben wir durch die modifizierte EventBase eine Methode erhalten, die es jedem Objekt ermöglicht, den jquery-Ereignisverwaltungsmechanismus zu unterstützen. Wenn wir in Zukunft die Verwendung des Ereignismechanismus zur Entkopplung in Betracht ziehen, müssen wir dies nicht mehr tun Denken Sie darüber nach. Das Publish-Subscribe-Modell wurde zuerst eingeführt. Diese Methode ist relativ leistungsfähiger und stabiler und entspricht eher Ihrer üblichen Gewohnheit, JQuery zum Betreiben von DOM zu verwenden.
4. Zusammenfassung dieses Artikels
Es gibt zwei Punkte, die erklärt werden müssen:
1) Auch wenn Sie jquery nicht verwenden, können Sie der am Ende von Teil 1 vorgeschlagenen Idee folgen und das reguläre Publish-Subscribe-Modell in Teil 1 umwandeln. Die Verwendung von jquery ist lediglich prägnanter;
Der obige Inhalt bezieht sich auf jQuery-Techniken, um es jeder Komponente zu ermöglichen, DOM-ähnliches Ereignismanagement zu unterstützen. Ich hoffe, dass er für alle hilfreich ist!