DIY

上一篇文章中,简要介绍了YUI实现AOP的Y.Do对象。

接下来,我们继续对YUI事件体系进行探索。本次要介绍的是Y.CustomEvent对象,从命名上就可以看出,这个对象在整个YUI事件体系中十分重要。它建立起整个自定义事件的体系,而且,DOM事件也构建在这个体系之上。

Y.Subscriber

Y.Subscriber的作用比较简单:执行回调函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 例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();
});

有些事件只需触发一次,比如你的各种第一次~~~。来看这个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 例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 happened
birthEvent.fire();

// 只触发一次的事件在触发后,再次添加监听方法时,会被立即执行
// output: before birth, too
var onBirthHandle2 = birthEvent.on(function () {
Y.log('before birth, too');
});
});

也许你还在琢磨,事件广播是什么?因为YUI使用了sandbox设计,可以生成不同实例绑定不同api,所以才有了事件广播机制。来看这个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 例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之处,那么用起来如何呢,来看下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 例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的内部实现吧。

注:为了更容易的看懂代码的核心,我做了适当的简化,感兴趣的朋友可以去看未删节的源码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
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) {
// 仅触发一次的事件,如果已经触发过,直接返回true
return 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) {
// 立即停止处理后续回调,方便实现stopImmediatePropagation
return 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 handle
if (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”。

另外,如果对于性能特别关心,在可能的情况下,尽量使用回调。

参考