DOM 事件模型及关联知识点

417 阅读3分钟

一、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 事件模型就不难理解,显然,它主要包括三个阶段:捕获阶段、目标阶段、冒泡阶段。 image.png

二、如何设置自定义事件

  一般来说,我们可以从 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>

image.png

  解决方法也很简单,通过遍历父元素的方法即可解决:如果 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()
})

Image.png

  • 目前页面右侧还是有滚动条,虽然不可以滚动,但仍然可以拉动,因此,要把滚动条取消掉;
  • 搜索 css hide scrollbar,得到以下方法:
::-webkit-scrollbar {
  width: 0 !important
}

Image.png

  • 此时,PC 端已完成设置,但客户端由于是触屏,所以仍需继续设置;
  • 手机触屏开始事件是 touchstart
EventTarget.addEventListener("touchstart",(e)=>{
    e.preventDefault()
})