一、DOM 事件模型
假设有以下代码,并且给每一个 div 都添加事件监听,分别为 fnDiv1、fnDiv2、fnDiv3。当我们点击文字的时候,是先调用 fnDiv1 还是先调用 fnDiv3?答案是都有可能。而事件调用时会经过一个个元素节点,这个传播过程可以称为事件流。
<div class = "div1">
<div class = "div2">
<div class = "div3">
文字
</div>
</div>
</div>
- IE5 浏览器默认先调用 fnDiv1,调用方式称为事件捕获,EventTarget.attachEvent( type, listener )。
- 网景浏览器默认先调用 fnDiv3,调用方式称为事件冒泡,EventTarget.addEventListener( type, listener )。
- W3C 统一了两种标准,默认先事件捕获再事件冒泡,EventTarget.addEventListener( type, listener, bool )。其中,在 W3C 标准中,可以通过 bool 设置是捕获事件还是冒泡事件,同时,事件冒泡还可以使用
e.stopPropagation()方法取消。 - 如果理解了什么是 DOM 事件流,那么 DOM 事件模型就不难理解,显然,它主要包括三个阶段:捕获阶段、目标阶段、冒泡阶段。
二、如何设置自定义事件
一般来说,我们可以从 MDN 文档的事件参考中查询有哪些事件,但是,如果要设置一个自定义事件,我们该如何实现?以下是实现代码:JSBin代码
<div id=div1>
<button id=button1>点击触发 frank 事件
</button>
</div>
button1.addEventListener('click', ()=>{
const event = **new CustomEvent**("frank", {"detail":{name:'frank', age: 18}})
button1.**dispatchEvent**(event)
})
button1.addEventListener('frank', (e)=>{
console.log('frank')
console.log(e)
console.log(e.detail)
})
三、事件委托和封装事件委托
一个 div 里面的 button 有很多个,假设要为每个 button 都绑定一个事件,这将会浪费大量的内存空间,DOM 操作速度也很慢,我们可以将监听绑定在祖先元素 div 上,委托 div 帮我们判断用户操作的对象是不是 button,是则执行对应的事件。将监听函数绑定在目标元素的祖先元素上的设计方法称为事件委托,实现代码如下: JSBin代码
<div id = "div1">
<span>span</span>
<button>button 1</button>
<button>button 2</button>
<button>button 3</button>
<button>button 4</button>
<button>button 5</button>
</div>
div1.addEventListener("click",(e)=>{
const t = e.target
if(t.tagName.toLowerCase()==="button"){ // 返回标签默认大写
console.log("button 被点击了,内容是:" + t.textContent )
}
})
封装事件委托,实现代码如下: JSBin代码
<div id=div1></div>
setTimeout(()=>{
const button = document.createElement("button")
button.textContent = "点击"
div1.appendChild(button)
},1000)
on("click","#div1","button",()=>{
console.log("button 被点击了")
})
function on(eventType,element,selector,fn){
if(!(element instanceof Element)){
element = document.querySelector(element)
}
element.addEventListener(eventType,(e)=>{
const t = e.target
if(t.matches(selector)){ //判断t和selector是否匹配
fn()
}
})
}
当 button 元素里面有 span 元素时,如果点击 span 元素,此时不会调用函数 fn,因为当前用户操作的元素为 span,显然,t 与 selector 不匹配。
<div id=div1>
<div id="div2">
<button id="button1">
点击会成功
<span id="span1">点击会失败</span>
</button>
</div>
</div>
解决方法也很简单,通过遍历父元素的方法即可解决:如果 t 与 selector 不匹配,则让 t 等于他的父元素,还是不匹配,t 就等于它的父元素的父元素,直到 t 与 selector 匹配,当然,如果遍历到最顶层元素 element 时,还是不匹配,则返回一个空值。JSBin代码
on("click", "#div1", "button", () => {
console.log("button 被点击了")
})
function on(eventType, element, selector, fn) {
if (!(element instanceof Element)) {
element = document.querySelector(element)
}
element.addEventListener(eventType, (e) => {
let t = e.target // 不能使用const声明变量
while (!t.matches(selector)) { //判断t和selector是否匹配
if (t === element) {
t = null
break
}
t = t.parentNode
}
t && fn.call(t,e,t) // 使用 call
})
}
四、阻止滚动的方法
- JSBin代码
- 滚动条作用的是 #document,所以在元素上使用 e.stopPropagation() 是没有用的;
- 使用 e.preventDefault(),意思是阻止默认事件;
- 由于监听的是事件,而不是行为,所以不能使用 scroll,要使用 wheel;
EventTarget.addEventListener("wheel",(e)=>{
e.preventDefault()
})
- 目前页面右侧还是有滚动条,虽然不可以滚动,但仍然可以拉动,因此,要把滚动条取消掉;
- 搜索 css hide scrollbar,得到以下方法:
::-webkit-scrollbar {
width: 0 !important
}
- 此时,PC 端已完成设置,但客户端由于是触屏,所以仍需继续设置;
- 手机触屏开始事件是 touchstart
EventTarget.addEventListener("touchstart",(e)=>{
e.preventDefault()
})