この記事では、任意のコンポーネント オブジェクトが DOM のようなイベント管理をサポートできるようにする jQuery のヒントを紹介します。つまり、イベントのディスパッチ、イベント リスナーの追加または削除に加えて、イベントのバブリングもサポートし、イベントのデフォルト動作を防止することもできます。など。 jquery の助けを借りて、このメソッドを使用して通常のオブジェクトのイベントを管理することは、DOM オブジェクトのイベントを管理することとまったく同じですが、最終的には、この小さなトリックの具体的な内容を見ると、それが正しいかどうかを感じるかもしれません。というケースもありますが、通常のパブリッシュ・サブスクライブモデルの実装をDOMライクなイベント機構に変更できれば、開発するコンポーネントの柔軟性や拡張性は確実に高まると感じていますし、私も初めて使用しましたが、この方法(洞察が浅すぎるので)、その価値はかなり大きいと思うのでシェアさせていただきました。
この手法を正式に紹介する前に、まず以前に検討した方法であるパブリッシュ/サブスクライブ モデルについて話して、それがどのような問題を解決できるか、また既存の問題を確認する必要があります。
1. パブリッシュ/サブスクライブ モデル
書籍を含む多くのブログでは、JavaScript がコンポーネントのカスタム イベントを実装したい場合は、パブリッシュ/サブスクライブ モデルを使用できると述べています。最初、私はそう信じていたので、jquery の $.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; });
コンポーネントがこの EventBase を継承している限り、そのコンポーネントは、以下で実装する FileUploadBaseView などの、メッセージのサブスクリプション、パブリッシュ、およびサブスクリプション解除の機能を完了するために提供される on off トリガー メソッドを継承できます。
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 リスナーがトリガー メソッドと追加の 2 つのパラメーターによってトリガーされました。リスナー内では、リスナーの関数パラメータを介してトリガーによって渡されたデータにアクセスするだけでなく、これを介して f オブジェクトにもアクセスできます。
現在の結果からすると、この方法は良さそうですが、FileUploadBaseView の実装を続けようとすると問題が発生しました。このコンポーネントを設計するときに使用したサブスクリプション関連のオプションを見てください:
私の元の設計は次のとおりです。これらのサブスクリプションはペアで定義され、サブスクリプションのペアは特定のインスタンス メソッドに対応します。たとえば、before を持つサブスクリプションは、対応するインスタンス メソッド (レンダリング) が呼び出される前にトリガーされ、 before のないサブスクリプションは、対応するインスタンス メソッド (render) が呼び出される前にトリガーされます。そのサブスクリプションは、対応するインスタンス メソッド (render) が呼び出された後にトリガーされます。また、before のあるサブスクリプションが false を返した場合、対応するインスタンスがトリガーされることも必要です。メソッドと後続のサブスクリプションは実行されません。最後の設計要件は、コンポーネントのインスタンス メソッドを呼び出す前に、たとえば、remove メソッドの呼び出し時に一部のデータを削除できないなどの特別な理由により、現在のインスタンス メソッドの呼び出しをキャンセルする必要がある場合があることを考慮することです。検証を行い、削除できる場合は true を返し、削除できない場合は false を返します。次に、次のアプローチと同様に、インスタンス メソッドで before サブスクリプションをトリガーした後に判断を追加します。 🎜>
しかし、このアプローチは純粋なコールバック関数モードでのみ実装できます。コールバック関数は 1 つの関数参照にのみ関連し、パブリッシュ/サブスクライブ モードでは、パブリッシュ/サブスクライブ モードでは機能しません。このアプローチをパブリッシュ/サブスクライブに適用すると、beforeRender に関連付けられたすべてのサブスクリプションが 1 回呼び出されます。その場合、どのサブスクリプションの戻り値が Accurate になりますか?おそらく、キュー内の最後のサブスクリプションの戻り値を基準として使用できると言われるでしょう。ほとんどの場合、これで問題ありませんが、「キューの最後のサブスクリプションの戻り値を基準として取得する」というロジックを追加すると、 EventBaseを使用する場合、外部で使用する場合はサブスクリプションの順序を明確に管理し、検証など特殊なロジックに関わるサブスクリプションを最後に配置しなければならないという大きなリスクが伴います。そして、文法やコンパイルとは何の関係もなく、コーディング順序に関する要件があるこの種の開発方法は、ソフトウェアに比較的大きなセキュリティ リスクをもたらします。いつでも、どのようなシナリオでも、サブスクリプションの順序を制御できることを誰が保証できますか。ましてや社内では、書く内容にそんな制限があることを知らない新人もいるかもしれません。この問題を解決する完璧な方法は、メッセージをオブジェクトにカプセル化し、そのすべてのサブスクリプションのうち、このメッセージを公開した後のロジックをブロックする必要があると考えられるオブジェクトに渡すことです。preventDefault() メソッドを呼び出すだけです。このメッセージの内容を確認し、メッセージを外部に公開した後、メッセージの isDefaultPrevented() メソッドを呼び出して判断します。
このアプローチは、jquery を使用して DOM オブジェクトのイベントを管理するのと同じです。たとえば、ブートストラップのほとんどのコンポーネントと、以前のブログで書いたコンポーネントは、アラート コンポーネントなどの追加の判断ロジックを追加するためにこのメソッドを使用します。 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(); });
このテストでは、DOM とは関係のない Demo コンポーネントを定義し、EventBase イベント管理クラスを継承しました。beforeRender イベントに 3 つのリスナーを追加しました。そのうちの 1 つは、predictDefault() の呼び出しとコールバックを持ちます。これは最後のテストではありません。最終的なテスト結果は次のとおりです:
結果からわかるように、render メソッドのメイン ロジック コードと後続の render イベントは実行されず、e.preventDefault() が有効になり、影響を受けないことがわかります。 beforeRender イベントキューに影響を与えます。
2 つのテストから判断すると、変更された EventBase を通じて、任意のオブジェクトが jquery イベント管理メカニズムをサポートできるようにするメソッドが得られました。将来、イベント メカニズムを使用して分離することを検討する場合、その必要はなくなります。パブリッシュ/サブスクライブ モデルが最初に導入されましたが、この方法は比較的強力で安定しており、jquery を使用して DOM を操作する通常の習慣により沿っています。
4. この記事の概要
説明する必要がある点は 2 つあります:
1) jquery を使用しない場合でも、パート 1 の最後で提案されたアイデアに従い、パート 1 で通常のパブリッシュ/サブスクライブ モデルを変換できます。jquery を使用する方がより簡潔であるというだけです。
2) 最後に、jquery のイベント メカニズムを使用してオブジェクトのイベント管理を実現する一方で、プロキシ モードが使用され、さらに重要なことにパブリッシュ/サブスクライブ モードが使用されますが、最終的な実装は次のように行われます。 jquery のパブリッシュ/サブスクライブ実装の最初の部分が変換されました。
上記の内容は、任意のコンポーネントが DOM のようなイベント管理をサポートできるようにするための jQuery テクニックに関連しています。これが皆さんのお役に立てば幸いです。