乐趣区

重探浏览器事件(浅析事件编程化)

前言
在平常开发过程中, 就算不使用现在主流的框架也至少得使用个 Jquery, 这些工具帮我们统一不同浏览器平台之间的差异和细节, 可以将注意力集中到开发上来.
不过有意思的一点是, 在看完高程的 N 年后我居然连 event 对象中的 target 和 currentTarget 属性的区别都忘记了.
先提几个引子:

你能说出 event.currentTarget 和 event.target 的区别吗?
如果可以那么 event.srcElement 和事件监听函数中的 this 呢
如何使用编程的方式来触发事件, 而不借助浏览器默认触发方式?
如何创建一个我们自己的 Event 对象, 然后自定义我们的事件?
实现上方的内容的同时该如何兼容 IE 浏览器?

如果这几个内容你都熟悉了, 那么这篇文章不会给你带来太多的帮助.
在正文开始之前先来浏览一个表格, 来看一下不同浏览器之间 Event 对象的属性有何不同:
<button id=”button”>click me then change word</button>
var button = document.getElementById(‘button’);

button.addEventListener(‘click’,function (event) {
console.log(event);
});
在下方的表格中我们记录了不同浏览器之间 click 点击后 event 可用的属性列表(删除了控制台输出的原型和函数引用):

firefox67
chrome72
edge44.17763.1.0
ie11
ie9

altKey
altKey
altKey
altKey
altKey

bubbles
bubbles
bubbles
AT_TARGET
AT_TARGET

button
button
button
bubbles
bubbles

buttons
buttons
buttons
BUBBLING_PHASE
BUBBLING_PHASE

cancelBubble
cancelBubble
cancelable
button
button

cancelable
cancelable
cancelBubble
buttons
buttons

clientX
clientX
clientX
cancelable
cancelable

clientY
clientY
clientY
cancelBubble
cancelBubble

composed
composed
ctrlKey
CAPTURING_PHASE
CAPTURING_PHASE

ctrlKey
ctrlKey
currentTarget
clientX
clientX

currentTarget
currentTarget
defaultPrevented
clientY
clientY

defaultPrevented
defaultPrevented
detail
constructor
constructor

detail
detail
eventPhase
ctrlKey
ctrlKey

eventPhase
eventPhase
fromElement
currentTarget
currentTarget

explicitOriginalTarget
fromElement
height
defaultPrevented
defaultPrevented

isTrusted
isTrusted
isPrimary
detail
detail

layerX
layerX
isTrusted
deviceSessionId
eventPhase

layerY
layerY
layerX
eventPhase
fromElement

metaKey
metaKey
layerY
fromElement
isTrusted

movementX
movementX
metaKey
height
layerX

movementY
movementY
movementX
hwTimestamp
layerY

mozInputSource
offsetX
movementY
isPrimary
metaKey

mozPressure
offsetY
offsetX
isTrusted
offsetX

offsetX
pageX
offsetY
layerX
offsetY

offsetY
pageY
pageX
layerY
pageX

originalTarget
path
pageY
metaKey
pageY

pageX
relatedTarget
pointerId
offsetX
relatedTarget

pageY
returnValue
pointerType
offsetY
screenX

rangeOffset
screenX
pressure
pageX
screenY

rangeParent
screenY
relatedTarget
pageY
shiftKey

region
shiftKey
returnValue
pointerId
srcElement

relatedTarget
sourceCapabilities
screenX
pointerType
target

returnValue
srcElement
screenY
pressure
timeStamp

screenX
target
shiftKey
relatedTarget
toElement

screenY
timeStamp
srcElement
rotation
type

shiftKey
toElement
target
screenX
view

srcElement
type
tiltX
screenY
which

target
view
tiltY
shiftKey
x

timeStamp
which
timeStamp
srcElement
y

type
x
toElement
target

view
y
twist
tiltX

which

type
tiltY

x

view
timeStamp

y

which
toElement

width
type

x
view

y
which

width

x

y

通过这个表格我们可以观察 Event 对象在不同浏览器之间结构是不同的, 出人意料的是即使是在现代浏览器中事件对象也存在着差异.
当然这篇文章可不是将所有的 Event 属性都将一遍, 要知道不同的事件的 Event 对象结构是不同的.
吐槽: 本来是打算提供 IE8 的但是 ie8 不能使用 addEventListener 来监听事件懒得去搞 ie 那套数据了.
currentTarget,target,srcElement,this
currentTarget
一句话:
哪个元素上监听的事件,event.currentTarget 返回的就是这个对象的本身的引用.
如果你的一个事件监听函数被注册到了多个 DOM 元素上, 利用这个属性你就可以判断是谁触发的事件.
this
回调函数中的 this === event.currentTarget.
button.addEventListener(‘click’,function (event) {
console.log(event.currentTarget === this); // true
});
target
event.target 和上面的三者不同, 这里面涉及到了 DOM 中的一个基本知识事件冒泡和事件拦截.
关于这两点我相信大家都已经了解了, 即使不了解网上介绍的文章也有一大堆.
我们用事件冒泡来举例, 并且改写我们之前的那个例子:
<div id=”wrap”>
<button id=”button”>test click</button>
</div>
var
wrap = document.getElementById(‘wrap’),
button = document.getElementById(‘button’);

// 注意我们监听的是 wrap 的 click 事件, 而不是 button 的 click 事件
wrap.addEventListener(‘click’,function (event) {

// event.target 指向的是按钮, 因为我们点击的是按钮
console.log(event.target === button && event.target === event.srcElement); // true

// 当我们点击按钮触发的事件冒泡到了 wrap, 所以触发了 wrap 的 click 事件,
// 此时 currentTarget 指向的是 wrap
console.log(wrap===this && wrap === event.currentTarget); // true

// 直接打印 event 然后控制台中查看 currentTaget 会返回 null
// 你可以将他赋值到一个变量在打印输出这个变量
// see https://github.com/vuejs/vue/issues/6867#issuecomment-338195468
})
在这个例子中, 我们点击页面中的按钮, 然后再按钮的包裹 div 中接收到了 button 冒泡上来的事件, 这其中:

this 和 currentTarget 指向的都是添加了监听器的对象这里就是 wrap
target 和 srcElement 指向的是触发了事件的元素

事件委托也是 event.target 最常见的用途之一:
// Make a list
var ul = document.createElement(‘ul’);
document.body.appendChild(ul);

var li1 = document.createElement(‘li’);
var li2 = document.createElement(‘li’);
ul.appendChild(li1);
ul.appendChild(li2);

function hide(e){
// e.target 引用着 <li> 元素
// 不像 e.currentTarget 引用着其父级的 <ul> 元素.
e.target.style.visibility = ‘hidden’;
}

// 添加监听事件到列表,当每个 <li> 被点击的时候都会触发。
ul.addEventListener(‘click’, hide, false);
https://developer.mozilla.org…
srcElement
简单理解 event.srcElement === event.target.

Event.srcElement 是标准的 Event.target 属性的一个别名。它只对老版本的 IE 浏览器有效。https://developer.mozilla.org…

参考之前的表格后看来这个属性还没有被干掉, 在目前最新的浏览器上它依然存在, 不过已经不建议使用, 除非你需要向下兼容.
完整的事件编程
EventTarget 接口
当我们在使用如下的方法的时候:

elem.addEventListener
elem.removeEventListener
elem.dispatchEvent

实际上是在使用 EventTarget 接口上的功能.
例如我们可以创建一个新的 EventTarget 对象来添加事件监听:
const a = new EventTarget;

a.addEventListener(‘click’,()=>{

})
但是这没有任何意义, 因为这里没有事件被触发. 我们仅仅是添加了事件监听器而已.
为了达到我们目的通过编程的方式来执行完整的事件流程我们还需要完成如下的几步:

继承 EventTarget 而不是直接使用 EventTarget 的实例,
在事件监听函数中传递 Event 对象
找个地方来触发这个事件

首先我们来继承 EventTarget 对象:
继承 EventTarget
在浏览器中大部分可以添加删除事件的对象都继承了 EventTarget 对象.
你可以在控制台选择一个 HTML 元素一路查找原型链得到.
但是他们进过重重继承, 都有自己的独特属性和事件类型甚至是不同的构造函数.
为了和已有的事件进行区分我们这里需要对 EventTarget 进行继承:
// — 包装 EventTarget 开始
function MyEventTarget() {
var target = document.createTextNode(null);
this.addEventListener = target.addEventListener.bind(target);
this.removeEventListener = target.removeEventListener.bind(target);
this.dispatchEvent = target.dispatchEvent.bind(target);
}
MyEventTarget.prototype = EventTarget.prototype;
// — 包装 EventTarget 结束

// — 创建我们继承 EventTarget 的构造函数
function myElem() {

}

myElem.prototype = new MyEventTarget;
myElem.prototype.constructor = myElem;

// 创建实例
const instance = new myElem();

instance.addEventListener(‘click’,()=>{
// 现在我们实例可以监听事件了
});

console.log(instance);
继承的过程看似非常复杂, 尤其是包装 EventTarget 显得多此一举. 但是搞定 EventTarget 的继承确实花了我大量的时间去寻找解决方案.
你完全可以编写自己的继承方式来去继承 EventTarget, 不过你会发现这其中的坑非常深.
简单来说,EventTarget 在 JavaScript 中真的就是一个接口, 虽然是以函数的形式存在, 但是它不是构造函数(这点在 Chrome64 和 firefox59 后进行了修改).
总之通过原型链继承的 EventTarget 统统无法工作, 如果使用 ES6 的类式继承在现代浏览器中 (Chrome64 和 firefox59 后) 都可以进行继承.
详细参考:https://stackoverflow.com/que…

创建我们的 Event 对象
获取一个 event:
document.getElementById(‘button’).addEventListener(‘click’,(event)=>{
// event
console.log(event);
})

如果你在浏览器中运行这段代码并且在控制台中查看, 你会发现变量 event 的名称 MouseEvent, 如果你沿着原型链向上你会发现继承的是 UIEvent 再次向上查看则是真正的 Event.
事件触发中传递的第一个参数我们通常叫它 event, 所有的 event 对象都基于 Event, 但是这不意味着这种关系的窗户纸就只有一层,click 事件中的 event 和 Event 之间就隔着一个 UIEvent.
通常随着 event 继承的层数越多,event 对象身上的属性也会越来越多.
现在我们来创建一个标准的 Event 对象:
// 使用全局的 Event
new Event(‘test’,{ // 事件类型
bubbles:false, // 是否冒泡 默认 false
cancelable:false,// 是否可以被取消 默认 false
});

https://developer.mozilla.org…
如果你在浏览器中观察这个对象, 你会发现事件上常见的属性诸如:

event.target
event.currentTarget
event.preventDefault()

都在这个 new Event()返回的对象中, 由于其他类型的事件都继承自 Event 这也解释了为什么事件对象中总是有这些属性.
和继承 EventTarget 一样, 使用 Event 的过程也同样艰难, 总的来说使用 Event 的难点在于它有两套 API:

第一套比较新的 API 提供了现代的接口, 也就是之前例子中的方式. 在创建一个已有的事件的时候, 你只需要使用全局的构造函数就可以, 例如:new MouseEvent(‘test’,/* 对应 MouseEvent 的参数选项 */), 但是缺点就是不支持 IE 浏览器.
第二套 API 支持 IE 浏览器, 但是使用过程比较繁琐使用 Event.createEvent(/* 事件类型 */)创建对应事件类型的 Event 对象, 使用 Event.initEvent()来初始化事件, 并且提供对应事件类型的参数, 如果你创建一个 MouseEvent 类型的事件 InitEvent 方法最多需要 15 个参数. 这种情况下使用 new MouseEvent()传入对象配置的形式就简单多了.

一篇值得参考的文章, 使用 createEvent apihttps://www.cnblogs.com/ggz19…

此外不同种类的事件, 都有自己的全局构造函数, 不同类型的构造函数的第二个参数中的选项也是不同的.
其他的构造函数请参考这里.
触发我们的事件
触发事件就显得简单多了, 我们需要使用 EventTarget.dispatchEvent 方法.
在我们之前创建的实例上进行事件的触发:
function MyEventTarget() {
var target = document.createTextNode(null);
this.addEventListener = target.addEventListener.bind(target);
this.removeEventListener = target.removeEventListener.bind(target);
this.dispatchEvent = target.dispatchEvent.bind(target);
}
MyEventTarget.prototype = EventTarget.prototype;
function myElem() {

}

myElem.prototype = new MyEventTarget;
myElem.prototype.constructor = myElem;

const instance = new myElem();

instance.addEventListener(‘test’, (event) => {
console.log(event); // 监听事件并且打印实例
});

const myEvent = new Event(‘test’); // 创建 Event 实例

instance.dispatchEvent(myEvent); // 触发事件

当你调用 dispatchEvent 的时候,EventTarget 会按照对应事件注册的顺序来同步执行这些事件监听器.
如果在事件监听器中调用了 event.preventDefault, 那么 dispatchEvent 就返回 false 反之返回 true(前提是 cancleable 为 true).
详细参考 https://developer.mozilla.org…

编程式的事件触发
我们在页面中来一次具体的实战, 首先建立如下的 HTML 结构:
<div id=”wrap”>
<button id=”button”>test click</button>
</div>

我们在 #wrap 中监听 click 事件, 然后在#button 触发 click 事件.
这样我们可以练习一下 Event 中 bubbles(允许冒泡)参数的使用,
另外还可以测试 click 事件中的 Event 对象如果不是 MouseEvent 的实例那么监听器是否会被触发.
const
button = document.getElementById(‘button’),
wrap = document.getElementById(‘wrap’);

wrap.addEventListener(‘click’, (event) => {
console.log(event); // 打印 event 对象
});

const myEvent1 = new Event(‘click’, {
bubbles: false, // 不可以冒泡
});

const myEvent2 = new Event(‘click’, {
bubbles: true, // 可以冒泡
});

button.dispatchEvent(myEvent1); // 这次没有打印出内容
button.dispatchEvent(myEvent2); // 这次打印出了内容

结论很明确:

dispatchEvent 执行的时候只要是 Event 的实例且类型相同那么监听器就会被触发.
bubbles 参数可以控制该事件是否允许冒泡

退出移动版