Home > Article > Web Front-end > jQuery tips to enable any component to support DOM-like event management_jquery
This article introduces a jquery tip that allows any component object to support DOM-like event management. That is to say, in addition to dispatching events, adding or deleting event listeners, it can also support event bubbling and prevent event default behaviors. etc. With the help of jquery, using this method to manage events of ordinary objects is exactly the same as managing events of DOM objects. Although in the end when you see the specific content of this little trick, you may feel that it is or is not the case, but I feel that if the implementation of the ordinary publish-subscribe model can be changed to a DOM-like event mechanism, the components developed will definitely have greater flexibility and scalability, and it is also the first time for me to use this method (insight Because it is too shallow), I think its value is quite great, so I shared it.
Before I formally introduce this technique, I must first talk about a method I considered before, which is the publish-subscribe model, to see what problems it can solve and its existing problems.
1. Publish-subscribe model
Many blogs, including books, say that if JavaScript wants to implement custom events for components, it can use the publish-subscribe model. At first, I firmly believed so, so I wrote one using jquery’s $.Callbacks:
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; });
(Based on seajs and the inheritance library class.js introduced in "Detailed Explanation of Javascript Inheritance Implementation")
As long as any component inherits this EventBase, it can inherit the on off trigger method it provides to complete the message subscription, publishing and unsubscription functions, such as the FileUploadBaseView I want to implement below:
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; });
實際呼叫測試如下:
測試中,實例化了一個FileUploadBaseView物件f,並設定了它的name屬性,透過on方法添加一個跟hello相關的監聽器,最後透過trigger方法觸發了hello的監聽器,並傳遞了額外的兩個參數,在監聽器內部除了可以透過監聽器的函數參數存取到trigger傳遞過來的數據,還能透過this存取f物件。
從目前的結果來說,這個方式看起來還不錯,但是在我想要繼續實作FileUploadBaseView的時候碰到了問題。你看我在設計這個元件的時候那幾個訂閱相關的option:
我原本的設計是:這些訂閱都是成對定義,一對訂閱跟某個實例方法對應,例如帶before的那個訂閱會在對應的實例方法(render)呼叫前觸發,不帶before的那個訂閱會在對應的實例方法(render)呼叫後觸發,而且還要求帶before的那個訂閱如果返回false,就不執行對應的實例方法以及後面的訂閱。最後這個設計要求是考慮到在調用組件的實例方法之前,有可能因為一些特殊的原因,必須得取消當前實例方法的調用,比如調用remove方法時有的數據不能remove,那麼就可以在before訂閱裡面做一些校驗,能刪除的回傳true,不能刪除的回傳false,然後在實例方法中觸發before的訂閱後加一個判斷就可以了,類似下面的這種做法:
但是這個做法只能在單純的回呼函數模式裡實現,在發布-訂閱模式下是行不通的,因為回調函數只會跟一個函數引用相關,而發布-訂閱模式裡,同一個訊息可能有多個訂閱,如果把這種做法應用到發布-訂閱裡面,當調用this.trigger('beforeRender')的時候,會把跟beforeRender關聯的所有訂閱全部調用一次,那麼以哪個訂閱的返回值為準呢?也許你會說可以用隊列中的最後一個訂閱的返回值為準,在大多數情況下也許這麼幹沒問題,但是當我們把“以隊列最後的一個訂閱返回值作為判斷標準”這個邏輯加入到EventBase中的時候,會出現一個很大的風險,就是外部在使用的時候,一定得清楚地管理好訂閱的順序,一定要把那個跟校驗等一些特殊邏輯相關的訂閱放在最後面才行,而這種跟語法、編譯沒有關係,對編碼順序有要求的開發方式會給軟體帶來比較大的安全隱患,誰能保證任何時候任何場景都能控制好訂閱的順序呢,更何況公司裡面可能還有一些後來的新人,壓根不知道你寫的東西還有這樣的限制。
解決這個問題的完美方式,就是像DOM對象的事件那樣,在消息發布的時候,不是簡簡單單的發布一個消息字符串,而是把這個消息封裝成一個對象,這個對象會傳遞給它所有的訂閱,哪個訂閱裡覺得應該阻止這個消息發布之後的邏輯,只要調用這個消息的preventDefault()方法,然後在外部發布完消息後,調用消息的isDefaultPrevented()方法判斷一下即可:
而這個做法跟使用jquery管理DOM對象的事件是一樣的思路,比如bootstrap的大部分組件以及我在前面一些博客中寫的組件都是用的這個方法來增加額外的判斷邏輯,比如bootstrap的alert元件在close方法執行的時候有一段這樣的判斷:
按照这个思路去改造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 this test, I defined a Demo component that has nothing to do with DOM and inherited the EventBase event management class. I added 3 listeners to the beforeRender event, one of which has a call to predictDefault(), and the callback It’s not the last one, the final test result is:
As can be seen from the results, the main logic code of the render method and the subsequent render event are not executed. All beforeRender listeners are executed, indicating that e.preventDefault() takes effect, and it does not affect the beforeRender event queue. Make an impact.
So judging from the two tests, through the modified EventBase, we have obtained a method that allows any object to support the jquery event management mechanism. In the future, when considering using the event mechanism for decoupling, we no longer need to think about it. The publish-subscribe model was introduced first, and this method is relatively more powerful and stable, and is more in line with your usual habit of using jquery to operate DOM.
4. Summary of this article
There are two points that need to be explained:
1) Even if you don’t use jquery, you can follow the idea proposed at the end of part 1 and transform the regular publish-subscribe model in part 1. It’s just that using jquery is more concise;
2) Finally, jquery’s event mechanism is used to realize event management of any object. On the one hand, the proxy mode is used, and more importantly, the publish-subscribe mode is used, but the final implementation is done by jquery for us. The first part of the publish-subscribe implementation has been transformed.
The above content is related to jQuery techniques to enable any component to support DOM-like event management. I hope it will be helpful to everyone!