YUI事件体系之Y.CustomEvent
上一篇文章中,簡要介紹了YUI實現AOP的Y.Do對象。
接下來,我們繼續對YUI事件體系進行探索。本次要介紹的是Y.CustomEvent對象,從命名上就可以看出,這個對象在整個YUI事件體系中十分重要。它建立起整個自定義事件的體系,而且,DOM事件也構建在這個體系之上。
Y.Subscriber
Y.Subscriber的作用比較簡單:執行回調函數。
Y.Subscriber = function (fn, context) {this.fn = fn; // 回調函數this.context = context; // 上下文this.id = Y.stamp(this); // 設置唯一id }; Y.Subscriber.prototype = {constructor: Y.Subscriber,// 執行回調函數notify: function (args, ce) {if (this.deleted) return null;var ret;ret = this.fn.apply(this.context, args || []);// 只監聽一次if (this.once) {ce._delete(this);}return ret;} };Y.CustomEvent
Y.CustomEvent主要作用是:建立自定義事件機制,為方便的進行事件創建、監聽、觸發提供良好基礎。自定義事件機制,實際上是Observer Pattern(Publish–subscribe Pattern的演化)的一種實現,這種機制能夠方便的實現模塊間解耦,增強模塊的擴展性。
YUI的自定義事件較其它一些js庫來說要強大一些,有這樣一些好的features:
- 支持事件接口(Event Facade),在回調函數中可以進行調用
- 支持設置默認執行方法
- 支持停止/立即停止傳播,并可設定停止傳播時執行的方法
- 支持阻止默認行為(默認執行方法),并可設定阻止默認行為時執行的方法
- 支持冒泡。指定冒泡目標序列,就可以順序的觸發事件(需要Y.EventTarget)
- 支持廣播。每個自定義事件,都可以設置在當前YUI實例范圍內和全局YUI內進行廣播
可以看出,YUI的自定義事件和DOM事件極其類似,這種設計自然到我們在用自定義事件時,絲毫感覺不到和DOM事件的差異。
示例
讓我們先來看個簡單的例子:
// 例1 簡單自定義事件 YUI().use('event-custom', function (Y) {var eatEvent = new Y.CustomEvent('eat');var onHandle = eatEvent.on(function () {Y.log('before eating');});var onHandle2 = eatEvent.on(function () {Y.log('before eating, too');});var afterHandle = eatEvent.after(function () {Y.log('after eating');}); // output: "before eating", "before eating, too", "after eating"eatEvent.fire();onHandle2.detach();// output: "before eating", "after eating"eatEvent.fire(); });有些事件只需觸發一次,比如你的各種第一次~~~。來看這個例子:
// 例2 僅觸發一次的自定義事件 YUI().use('event-custom', function (Y) {var birthEvent = new Y.CustomEvent('birth', {fireOnce: true // you can only birth once});var onBirthHandle = birthEvent.on(function () {Y.log('before birth');});// output: "before birth"birthEvent.fire();// nothing happenedbirthEvent.fire();// 只觸發一次的事件在觸發后,再次添加監聽方法時,會被立即執行// output: before birth, toovar onBirthHandle2 = birthEvent.on(function () {Y.log('before birth, too');}); });也許你還在琢磨,事件廣播是什么?因為YUI使用了sandbox設計,可以生成不同實例綁定不同api,所以才有了事件廣播機制。來看這個例子:
// 例3 事件廣播 YUI().use('event-custom', function (Y) {var cryEvent = new Y.CustomEvent('cry', {broadcast: 2 // global broadcast});cryEvent.on(function () {Y.log('before cry');});Y.on('cry', function () {Y.log('YUI instance broadcast');});Y.Global.on('cry', function () {Y.log('YUI global broadcast');});// output: "before cry", "YUI instance broadcast", "YUI global broadcast"cryEvent.fire(); });文章之前介紹過YUI自定義事件的種種NB之處,那么用起來如何呢,來看下面的例子:
// 例4 復雜自定義事件 YUI().use('event-custom', function (Y) {var driveEvent = new Y.CustomEvent('drive', {emitFacade: true,host: { // hacking. 復雜自定義事件需要指定host,該host必須augment Y.EventTarget_yuievt: {},_monitor: function () {}},defaultFn: function () {Y.log('execute defaultFn');},preventedFn: function () {Y.log('execute preventedFn');},stoppedFn: function () {Y.log('execute stoppedFn');}});driveEvent.on(function (e) {e.stopImmediatePropagation();});driveEvent.on(function (e) {e.preventDefault();});driveEvent.after(function (e) {Y.log('after driving');});// output: "execute stoppedFn", "execute defaultFn"driveEvent.fire(); });不要失望,現在還沒有介紹到事件體系的精華部分Y.EventTarget,所以很多特性(例如冒泡)還不能體現出來,拭目以待吧。
源代碼分析
接下來,讓我們看看YUI的內部實現吧。
注:為了更容易的看懂代碼的核心,我做了適當的簡化,感興趣的朋友可以去看未刪節的源碼。
var AFTER = 'after',// config白名單CONFIGS = ['broadcast', 'monitored', 'bubbles', 'context', 'contextFn', 'currentTarget', 'defaultFn', 'defaultTargetOnly', 'details', 'emitFacade', 'fireOnce', 'async', 'host', 'preventable', 'preventedFn', 'queuable', 'silent', 'stoppedFn', 'target', 'type'];Y.CustomEvent = function (type, o) {this.id = Y.stamp(this);this.type = type;this.context = Y;this.preventable = true;this.bubbles = true;this.subscribers = {}; // (前置)監聽對象容器 注:YUI3.7.0將此處進行了優化this.afters = {}; // 后置監聽對象容器 注:YUI3.7.0將此處進行了優化this.subCount = 0;this.afterCount = 0;o = o || {};this.applyConfig(o, true); }; Y.CustomEvent.prototype = {constructor: Y.CustomEvent,// 設置參數applyConfig: function (o, force) {if (o) {Y.mix(this, o, force, CONFIGS);}},// 添加前置監聽對象on: function (fn, context) {var a = (arguments.length > 2) ? Y.Array(arguments, 2, true) : null;return this._on(fn, context, a, true);},// 添加后置監聽對象after: function (fn, context) {var a = (arguments.length > 2) ? Y.Array(arguments, 2, true) : null;return this._on(fn, context, a, AFTER);},// 內部添加監聽對象_on: function (fn, context, args, when) {var s = new Y.Subscriber(fn, context);if (this.fireOnce && this.fired) {// 僅觸發一次的事件在觸發后,再次添加監聽方法時,會被立即執行this._notify(s, this.firedWith);}if (when == AFTER) {this.afters[s.id] = s;this.afterCount++;} else {this.subscribers[s.id] = s;this.subCount++;}return new Y.EventHandle(this, s);},// 觸發事件fire: function () {if (this.fireOnce && this.fired) {// 僅觸發一次的事件,如果已經觸發過,直接返回truereturn true;} else {// 可以設置參數,傳給回調函數var args = Y.Array(arguments, 0, true);this.fired = true;this.firedWith = args;if (this.emitFacade) {// 復雜事件return this.fireComplex(args);} else {return this.fireSimple(args);}}},// 觸發簡單事件fireSimple: function (args) {this.stopped = 0;this.prevented = 0;if (this.hasSubs()) {var subs = this.getSubs();// 處理前置監聽對象this._procSubs(subs[0], args);// 處理前置監聽對象this._procSubs(subs[1], args);}this._broadcast(args);return this.stopped ? false : true;},// 判斷是否有監聽對象hasSubs: function (when) {var s = this.subCount,a = this.afterCount;if (when) {return (when == 'after') ? a : s;}return (s + a);},// 獲取所有前置/后置監聽對象getSubs: function () {var s = Y.merge(this.subscribers),a = Y.merge(this.afters);return [s, a];},// 獲取監聽對象_procSubs: function (subs, args, ef) {var s, i;for (i in subs) {if (subs.hasOwnProperty(i)) {s = subs[i];if (s && s.fn) {if (false === this._notify(s, args, ef)) {// 回調返回false時,立即停止處理后續回調this.stopped = 2;}if (this.stopped == 2) {// 立即停止處理后續回調,方便實現stopImmediatePropagationreturn false;}}}}return true;},// 通知監聽對象,執行回調方法_notify: function (s, args, ef) {var ret = s.notify(args, this);if (false === ret || this.stopped > 1) {return false;}return true;},// 廣播事件_broadcast: function (args) {if (!this.stopped && this.broadcast) {var a = Y.Array(args);a.unshift(this.type);// 在當前YUI實例Y上廣播if (this.host !== Y) {Y.fire.apply(Y, a);}// 在全局對象YUI上廣播,跨實例if (this.broadcast == 2) {Y.Global.fire.apply(Y.Global, a);}}},// TODO: 在下一篇介紹Y.EventTarget的文章中再做介紹fireComplex: function (args) {},// 移除監聽器detach: function (fn, context) {// unsubscribe handleif (fn && fn.detach) {return fn.detach();}var i, s,found = 0,subs = Y.merge(this.subscribers, this.afters);for (i in subs) {if (subs.hasOwnProperty(i)) {s = subs[i];if (s && (!fn || fn === s.fn)) {this._delete(s);found++;}}}return found;},_delete: function (s) {if (s) {if (this.subscribers[s.id]) {delete this.subscribers[s.id];this.subCount--;}if (this.afters[s.id]) {delete this.afters[s.id];this.afterCount--;}}if (s) {s.deleted = true;}} };適用場景
自定義事件的適用場景與Publish–subscribe Pattern基本一致。具體來講,我覺得以下一些場景是非常適合用自定義事件的:
a) 需要暴露接口/行為以滿足擴展需要
底層模塊一般會設計的盡量簡單,解決核心問題,并適當的開放一些接口,方便應用層進行擴展以滿足實際需求。例如表單驗證控件,有可能需要在某個表單項驗證成功/失敗后執行一些額外操作,舉一個實際的例子:當用戶輸入的郵箱地址驗證成功時,我們會檢查是不是某些比較爛的郵件服務商,如果是則給出一些建議。
YUI作為一個底層基礎庫,在組件/控件層面加入了大量的自定義事件,以滿足實際應用中的需要。例如Y.Anim的start、end事件,Y.io的success、failure、end事件,Y.Attribute中的屬性變化事件等。
b) 行為可能會被其它模塊/方法中止
這一點非常像DOM事件,我們經常會中止一些事件的默認行為,例如anchor的點擊事件。
自定義事件 VS 回調函數
這是一個比較難的問題,我自己的看法是:相對回調函數,自定義事件是一種更重但更靈活的方案。在實際應用中,如果對于關心某消息的受眾不夠清楚,那么就使用事件。否則,比較適合使用回調函數。
MSDN上的解釋更好一些:“An event is like an anonymous broadcast, while a call-back is like a handshake. The corollary of this is that a component that raises events knows nothing about its clients, while a component that makes call-backs knows a great deal”。
另外,如果對于性能特別關心,在可能的情況下,盡量使用回調。
參考
- YUILibrary-CustomEvent
- YUILibrary-EventTarget
- Wikipedia-Publish–subscribe Pattern
- Zakas-Custom events in JavaScript
- When to Use Events or Call-Backs for Notifications
總結
以上是生活随笔為你收集整理的YUI事件体系之Y.CustomEvent的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Redis系列教程(五):Redis哨兵
- 下一篇: 机器学习从理论到工程的第一步-编程语言篇