事件的三个阶段
捕获阶段
-> 目标阶段
-> 冒泡阶段
,IE低版本不支持捕获阶段。addEventListener
的第三个参数useCapture
就是表示是否在捕获阶段触发,默认false。
事件的绑定和解绑
一般有3种写法。
属性方式
此方式不推荐。如,直接在div上面写onclick:
<input type="button" value="测试" onclick="test(event)"/>
<script>
function test(e) {
console.log(e);
alert('你好!');
}
</script>
或者通过JS来绑定onclick
:
target.onclick = fn; // 绑定事件
target.onclick = null; // 解绑
return false; // 阻止默认事件
W3C标准:
target.addEventListener(eventName, fn[, useCapture]); // 绑定事件
target.removeEventListener(eventName, fn[, useCapture]); // 解绑
e.preventDefault(); // 阻止默认事件
e.stopPropagation(); // 阻止冒泡
// 获取event对象:第一个参数就是e
IE8或更低:
不支持捕获阶段
target.attachEvent('on'+eventName, fn); // 绑定事件
target.detachEvent('on'+eventName, fn); // 解绑
e.returnValue = false; // 阻止默认事件
e.cancelBubble = true; // 阻止冒泡
// 获取event对象:window.event
封装
/**
* 绑定事件
*/
function on(target, eventName, fn)
{
if(!target) return;
if(target.addEventListener) target.addEventListener(eventName, fn, false);
else if(target.attachEvent) target.attachEvent('on'+eventName, fn); // 如果IE8或者更低版本
else target['on'+eventName] = fn;
}
/**
* 解绑事件
*/
function off(target, eventName, fn)
{
if(!target) return;
if(target.removeEventListener) target.removeEventListener(eventName, fn, false);
else if(target.detachEvent) target.detachEvent('on'+eventName, fn); // 如果IE8或者更低版本
else target['on'+eventName] = null;
}
事件内部兼容写法
on($0, function(e) {
e = e || window.event;
var target = e.target || e.srcElement; // srcElement是老版本IE写法
console.log(e, target);
});
详述useCapture
早期版本的useCapture
不是可选的,为了兼容性最好始终写上它。
// TODO
移除事件需要注意的问题
removeEventListener时必须保证fn和原来的一样,否则remove不会生效;另外,如果同一个监听事件分别为“事件捕获”和“事件冒泡”注册了一次,那么这两次事件需要分别移除一次。
初学者可能经常会碰到一个问题,就是调用了removeEventListener
之后事件依然存在,出现这个问题的一般原因都是add
和remove
时fn
的引用不一致导致的,举个栗子:
// 切换事件
function toggleEvent(flag) {
function fn(e) {
console.log(e.target);
}
if(flag) window.addEventListener('click', fn, false);
else window.removeEventListener('click', fn, false);
}
toggleEvent(true); // 添加事件
// 随便点击页面空白处几次测试效果
toggleEvent(false); // 移除事件
// 再次点击页面空白处发现事件没有移除成功
上面的代码想当然的把fn拿出来,以为这样写就能够正常移除了,但是由于每次执行的时候相当于重新定义了一个fn
,虽然代码一模一样,但是二者不相等,所以移除失败,这就好比:
var a = function(){};
var b = function(){};
a === b; // false
委托
这段委托代码还有问题,先放这里:
function delegate(obj, eventName, selector, fn)
{
on(obj, eventName, function(e)
{
e = e || window.event;
var target = e.target || e.srcElement;
var objs = document.querySelectorAll(selector);
for(var i=0; i<objs.length; i++)
{
if(objs[i] == target) fn.apply(target, e);
}
});
}
JS模拟事件的实现
ES6版的:
class EventEmitter {
constructor() {
this._events = {};
}
on(type, fn) {
this._events[type] = this._events[type] || [];
this._events[type].push(fn);
}
off(type, fn) {
if(fn === undefined) this._events[type] = [];
else {
let callbacks = this._events[type] || [];
let idx = callbacks.indexOf(fn);
if(idx >= 0) {
callbacks.splice(idx, 1);
}
}
}
emit(type, ...args) {
(this._events[type] || []).forEach(fn => fn.apply(this, args));
}
}
测试:
var emitter = new EventEmitter();
var fn1 = function(a, b) { console.log('我是第1个', a, b); }
var fn2 = function(a, b) { console.log('我是第2个', a, b); }
emitter.on("myevent", fn1);
emitter.on("myevent", fn2);
emitter.emit('myevent', 111, 222);
emitter.off('myevent', fn1);
emitter.emit('myevent', 111, 222);
参考
https://developer.mozilla.org/zh-CN/docs/Web/API/EventTarget/addEventListener