JavaScript原生事件相关知识点总结
本文由 小茗同学 发表于 2016-11-12 浏览(3852)
最后修改 2020-02-13 标签:javascript 事件 绑定 解绑 委托 代理

事件的三个阶段

捕获阶段 -> 目标阶段 -> 冒泡阶段,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之后事件依然存在,出现这个问题的一般原因都是addremovefn的引用不一致导致的,举个栗子:

// 切换事件
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

http://www.cnblogs.com/zhangmingze/p/4864367.html