EventTarget.addEventListener()
我们在学习addEventListener()
时都只是知道它是用来给事件注册事件处理函数的。但是这种描述并不是很准确,MDN上给我们准确的描述了它的定义。EventTarget.addEventListener()
方法将指定的监听器注册到EventTarget
上,当该对象触发指定的事件时,指定的回调函数就会被执行。EventTarget
目标对象可以是一个文档上的元素ELement、Document、Window
或者是任何其它支持事件的对象,例如XMLHTTPRequest
。
addEventListener()
的工作原理是将实现EventListener
的函数或者对象添加到调用它的EventTarget
上的指定事件类型的事件侦听器列表中。
EventTarget
EventTarget
是一个DOM接口,由可以接收事件、并且可以创建侦听器的对象实现。
Element、Document、Window
是最常见的EventTargets
,但是其它的对象也可以作为EventTargets
,比如XMLHTTPRequest、AudioNode、AudioContext
等等。
许多EventTargets
包括(elements、documents、windows
)支持通过onEvent
特性和属性设置事件处理函数event handlers
。
首先EventTarget()
是一个构造函数,通过实例化new EventTarget()
构造函数创建一个新的EventTarget
实例对象,在EventTarget.prototype
上存在三个方法:addEventListener()
、dispatchEvent()
、removeEventListener()
。addEventListener()
方法在EventTraget
上注册特定的事件类型的事件处理函数。removeEventListener()
方法删除EventTarget
事件处理函数。dispatchEvent()
将事件分派到EventTarget
。
那么EventTarget
事件目标构造函数存在的意义是什么呢?我们看下面的例子中,通过EventTarget
构造函数实例化的对象可以继承EventTarget.prototype
方法,那么就说明此时的obj
对象是一个EventTarget
对象。而一个普通对象,因为不是EventTarget
对象,所以不能够继承到EventTarget.prototype
的方法。
EventTarget
构造函数的第一个作用就是创建EventTarget
对象。- 让其它的
EventTargets
对象通过原型链的方式继承到EventTarget.prototype
上的方法,例如window、document、Element
。
const obj = new EventTarget();
obj.addEventListener();const obj = {};
obj.addEventListener(); // Uncaught TypeError: obj.addEventListener is not a function.
为什么window
对象可以通过window.addEventListener()
的形式注册事件处理函数?window
对象种并不存在addEventListener()
方法,而addEventListener()
方法存在EventTarget.prototype
上,但是由于原型链的作用。window
对象通过原型链的方式从EventTarget.prototype
上继承addEventListener()
方法,这也正是window
对象为什么能够调用addEventListener()
方法的原因。
console.log(window);
EventTarget的工作方式及简单实现
EventTarget
的工作方式主要是利用EventTarget.prototype
上的方法。
addEventListener()
方法:在EventTarget
上注册特定事件类型的事件处理程序。
removeEventListener()
方法:在EventTarget
中删除事件侦听器事件处理函数。
dispatchEvent()
方法:将事件派发到EventTarget
上。
熟悉方法之后,我们就要将这几个方法进行重写。
封装EventTarget
构造函数的思路是什么呢?首先我们需要分析事件到底是处于什么样的时机触发的呢?我们如何去执行事件触发之后的事件处理函数?其实也是很简单的,我们没有办法具体的控制事件处理函数执行的时机,因为事件处理函数是在事件触发的时候执行的,但是我们不知道事件具体是在什么时候触发的。那么如何处理呢?我们可以通过数组的方式,将该事件类型绑定的事件处理函数都存放到数组内部保存,当事件派发dispatch
的时候,将数组中存放的事件处理函数都拿出来执行。
/**
* EventTarget构造函数
* listeners:存放事件类型,事件回调函数的对象
*/
var EventTarget = function() {this.listeners = {};
}/**
* 为什么在prototype重写声明一遍listeners,因为让其它EventTargets对象继承,
* 例如:window、document
*/
EventTarget.prototype.listeners = null;/**
* @description: addEventListener
* type事件类型不存在listener中,我们就创建一个数组,用来存放该事件回调函数;
* type事件类型存在listener中,将事件回调函数放入对应的事件类型数组中;
* @param {*} type: 事件类型
* @param {*} callback: 事件回调函数
* @return {*} undefined
*/
EventTarget.prototype.addEventListener = function(type, callback) {// 事件类型是否存在listeners中if (!(type in this.listeners)) {this.listeners[type] = [];}this.listeners[type].push(callback);
}/**
* @description: removeEventListener
* 移除绑定的事件处理函数,注意 stack[i] === callback,
* 这就是为什么你需要移除事件监听函数时,必须在addEventListener绑
* 定事件处理函数是具名函数的原因,因为匿名函数无法判断是否相等。
* @param {*} type: 事件类型
* @param {*} callback: 事件回调函数
* @return undefined
*/
EventTarget.prototype.removeEventListener = function(type, callback) {// 事件类型是否存在listeners中if (!(type in this.listeners)) {return;}// stack表示该事件回调数组[]var stack = this.listeners[type],len = stack.length;for (var i = 0; i < len; i++) {if (stack[i] === callback) {this.listeners.splice(i, 1);} }
}/**
* @description: dispatchEvent
* 向一个指定的事件目标派发一个事件,
* 并以合适的顺序同步调用目标元素相关的
* 事件处理函数。
* @param {*} event:要派发的事件对象
* @param {*} target:用来初始化事件和决定将会触发目标
*/
EventTarget.prototype.dispatchEvent = function(event) {// 事件类型是否存在listeners中if (!(event.type in this.listeners)) {return;}var stack = this.listeners[event.type],len = stack.length;event.target = this;for (var i = 0; i < len; i++) {stack[i].call(this, event);}
}
EventTarget.dispatchEvent()深入到Event构造函数
EventTarget.dispatchEvent()
方法与浏览器原生事件有什么不同?浏览器原生事件,是由DOM
派发的,并通过Event loop
异步调用事件处理程序,而dispatchEvent()
则是同步调用事件处理程序。在调用dispatchEvent()
后,所有监听该事件的事件处理程序将在代码前执行返回。
dispatchEvent()
方法是create-init-dispatch
过程中的最后一步,用于将事件调用到实现的事件模型中。可以利用Event
构造函数创建事件。这是MDN
文档上对于dispatchEvent
方法的介绍,既然介绍到了Event
构造函数,我们就一起来看看如何自定义事件对象Event
。
注意一哈,事件是否能够取消,事件处理函数中是否阻止过事件默认行为,这都是可以获取到的。e.cancelable
作为Event
实例的只读属性,表明事件是否可以被取消。e.defaultPrevented
判断处理函数中是否阻止过事件默认行为,换句话说就是在事件处理函数中是否调用过e.preventDefault()
方法。
Event()构造函数,创建一个新的事件对象Event。event = new Event(typeArg, eventInit);typeArg: 表示所创建事件的名称。eventInit:是EventInit类型的字典,接受以下的字段:·"bubbles",可选,Boolean类型,默认值为false,表示事件是否冒泡。·"cancelable",可选,Boolean类型,默认值为false,表示该事件是否能被取消。·"composed",可选,Boolean类型,默认值为false,指示事件是否会在影子DOM根节点之外触发侦听器。
熟悉Event
构造函数,我们来尝试自定义一个事件,然后利用dispatchEvent()
方法将事件派发到EventTarget
对象上。下面例子中,我自定义了一个事件see
,并且这个see
事件支持冒泡,不支持取消,通过dispatchEvent
方法将事件派发到EventTarget
对象(oDiv
)上,此时oDiv
元素就能够监听到我自定义的see
事件。
注意下面例子中,虽然事件see
的事件处理程序中调用了e.preventDefault()
方法,但是e.defaultPrevented
依旧返回false
,这是为什么?因为see
事件在定义的时候,我们将cancelable
字段设置为false
,也就是表明事件see
不可取消,事件处理程序中无法监听回调中停止事件。所以,e.defaultPrevented
字段的结果返回false
。
var ev = new Event('see', {bubbles: true,cancelable: false
});var oDiv = document.getElementsByTagName('div')[0];oDiv.addEventListener('see', function(e) {e.preventDefault();console.log(e.defaultPrevented); // falseconsole.log('Listening event see....');console.log(e.cancelable); // false 事件不可取消
});oDiv.dispatchEvent(ev);
EventTarget.addEventListener()深入到滚屏优化
MDN
文档上指出EventTarget.addEventListener()
方法将指定的监听器注册到EventTarget
上,当该对象触发指定的事件时,指定的回调函数就会被执行。事件目标可以是一个文档上的元素Element、Document、Window
或者任何支持事件的对象,比如XMLHTTPRequest
。
addEventListener()
工作原理是将实现EventListener
的函数或对象添加到调用它的EventTarget
上指定事件类型的事件侦听器列表中,与我们上面重写addEventListener()
方法的逻辑一致。
上面简述是MDN
文档对addEventListener()
方法的定义,我们之前学习addEventListener()
方法时并没有仔细的看addEventListener()
方法的参数,Vue中的事件修饰符与addEventListener()
方法中的options
参数特别相似。
addEventListener(eventType, handler, useCapture || options);
这是addEventListener()
方法标准的语法,其中eventType
表示监听的事件类型,hanlder
表示事件处理函数,useCapture
表示事件流中两种事件传播方式,false
表示选择事件冒泡的方式触发事件处理函数,true
表示选择事件捕获的方式触发事件处理函数。
oDiv.addEventListener('click',function(){},false);
上面的例子中,是我们最常用的方式。但是在DOM4
的标准里,addEventListener()
方法中的还可以设置options
参数,options
参数表示:一个指定有关listener
(事件处理程序) 属性的可选参数对象。可选的参数默认都是false
,可选参数有:
capture
:Boolean
,表示listener
会在该类型的事件捕获阶段阶段传播到该EventTarget
时触发,与我们上面分析的useCapture
是同一个意思。
once
:Boolean
,表示listener
在添加之后最多调用一次。如果是true,listener
会在其被调用之后自动移除。
passive
:Boolean
,设置为true
时,表示listener
永远不会调用ev.preventDefault()
方法,如果你仍然在listener
调用了ev.preventDefault()
方法,浏览器会在控制台中抛出警告unable to prevetDefault inside passive event listener invocation;
(无法在被动事件侦听器调用内预先设置默认值)。
滚屏优化
在学习addEventListener()
方法中的passive
字段时,MDN
文档中上提出了一个“使用passive改善滚屏性能”的概念。
:::info
下面例子中是在Chrome浏览器下中测试的结果。
:::
在了解“使用passive
改善的滚屏性能”概念之前,我们先看下面的例子。我们在window
对象上增加touchstart
事件的事件处理函数function(e){}
。只要我们触摸到屏幕开始滚动的时候,就会执行调用绑定的事件处理函数function(e){}
。
window.addEventListener('touchstart',function(e){console.log('Listening scroll....');
});
那么我现在想阻止touchstart
事件的默认行为,touchstart
事件的默认行为是什么呢?touchstart
事件的默认行为其实就是srcoll
滚动。因为你触摸点击屏幕开始滚动的时候,touchstart
事件的listener
侦听器就开始执行。那我现在尝试阻止touchstart
事件的默认行为,例如下面的例子。
我在事件处理函数listeners
中增加了e.preventDefault()
语句,希望阻止touchstart
事件的默认行为,也就是说我不想让屏幕滚动了,并且我还在listeners
中增加e.defaultPrevented
语句判断是否调用过e.preventDefault()
方法。但是结果如下面的截图一样,这样的方式不仅仅没有成功的阻止touchstart
事件的默认行为,而且浏览器还抛出了异常。这是为什么?
我们通过对异常的分析,错误提示我们:passive
无法阻止去调用默认行为,事件处理函数listener
取决于目标开始时的passive
状态。换句话来说,就是passive
字段现在的状态相当于设置成true
,因为上面我们说过,passive
是true
的时候,listener
永远不会调用ev.preventDefault()
,如果你强制调用,则会抛出异常。
window.addEventListener('touchstart',function(e){e.preventDefault();console.log('Listening scroll....');console.log(e.defaultPrevented); // false
});
为什么会出现上述的这种情况呢?
根据MDN
文档,passive
选项的默认值始终为false
。但是某些浏览器(特别是Chrome和Firefox)已将文档级节点Window、Document、Document.body
的touchstart
和touchmove
事件的passive
选项默认值设置为true
。所以上面两种情况在listeners
中调用ev.preventDefault()
方法并不能够成功阻止事件的默认行为。但是浏览器为什么要这样做?
因为在监听touchstart
事件的时候,用户如果触发了touchstart
事件,那么touchstart
事件绑定的处理函数listeners
在执行的时候,内部如果有阻止默认行为的代码时,就不会再去执行默认行为了。如果内部没有阻止默认行为的代码时,下一步就会执行默认行为。但是无论是否阻止默认行为,它都会有一个等待的时间,因为必须等待listener
执行程序完成后,再去执行默认行为(滚动),此时等待的时间会造成滚动的卡顿。所以执行的顺序是:listeners
—> 执行默认行为,所以这种执行顺序在主线程中内部存在非常大的性能问题,由于有等待的时间,导致滚动卡顿。所以有些浏览器针对这种问题,将passive
默认值改为true
。
那么将passive
设置为true
有什么好处?passive
设置为true
时,程序会开启两个线程进行处理滚动的问题,一个线程是处理listeners
的执行,一个线程是处理执行默认行为,所以正是因为这个原因,MDN
文档上指出“passive
优化改善滚屏性能”的概念。
下面的例子,成功的取消了touchstart
事件的默认行为。注意首先我们说以上测试的结果都是在chrome
浏览器中,其次我们需要掌握的不是如何取消touchstart
的默认行为,而是passive
为什么能够改善滚屏性能的原因?最后要了解addEventListener()
方法中的options
参数。
window.addEventListener('touchstart',function(e){e.preventDefault();console.log('Listening scroll....');console.log(e.defaultPrevented); // false
},{passive:false
});