前言

事件的本质是程序各组成部分间的一种通信方式;本文主要介绍DOM事件级别,DOM事件模型,DOM事件流,以及常见应用等,希望对我们有所帮助。

一、DOM事件级别

DOM标准已有4个,其中DOM1标准没有涉及事件相关的修改

1. DOM 0级别

el.onclick = function () {...}
// html -on方式
<button class="btn0" onclick="dom0()"></button>
<script>
    function dom0() {
      alert('dom0')
    }
</script>
// js函数式
document.querySelector('.btn0').onclick = dom0

2. DOM 2级别

el.addEventListener('evnet-name', callback, [, useCapture])*
  <button class="btn2">dom2</button>
  <script>
    let btn2 = document.querySelector('.btn2')
    btn2.addEventListener('click', function () {
      alert('dom2')
    })
  </script>
//IE9-:attachEvent()与detachEvent()。
//IE9+/chrom/FF:addEventListener()和removeEventListener()
IE9.0以下的IE浏览器不支持

3. DOM 3级别

对DOM2进行拓展,添加更多的事件类型
  • UI事件类,当用户与页面上的元素交互时触发,如:load、scroll
  • 鼠标事件,当用户用鼠标在页面执行操作时触发,如:dbclick、mouseup
  • 键盘事件,当用户操作键盘在页面上执行操作时触发,如:keydown、keypress
  • 滚动事件,当用户滚动鼠标滚轮或类似设备时触发,如:mousewheel
  • 焦点事件,当元素获取或失去焦点时触发,如:focus、blur
  • 文本事件,当文档控件输入文本时触发,如:textInput
  • 合成事件,当IME(输入法编辑器)输入字符时触发,如:compositionstart
  • 变动事件,当底层DOM结构发生变化时触发,如:DOMsubstreeModified
  • 自定义事件
小结
DOM0时期:一个事件只能监听一个函数,且只有冒泡阶段
<div onclick="console.log('div')">
    <button class="btn" onclick="console.log('btn')">dom0</button>
</div>
// btn 
// div
DOM2之后:可以同事监听多个函数,且支持捕获和冒泡阶段
意义:DOM2标准的出现,统一了js的监听函数接口,也日益复杂的交互做好了铺垫。

二、DOM事件模型

捕获模型和冒泡模型

三、DOM事件流

一个事件发生后,会在子元素和父元素之间传播;经历捕获、目标、冒泡阶段。
  1. 捕获阶段:从window对象自顶向下朝目标元素传播阶段。
  2. 目标阶段:在目标元素处理事件阶段。
  3. 冒泡阶段:从目标元素自底向上朝window对象传播阶段。

68747470733a2f2f757365722d676f6c642d63646e2e786974752e696f2f323031382f31312f392f313636663831663365306432643163613f773d34363126683d32343326663d706e6726733d3332363530.png

提问:事件在捕获阶段有哪些实际案例,希望有老铁可以分享?

四、Event对象常见属性与应用

事件发生以后会产生一个Event对象,其常见方法/属性如下:
  • event.preventDefault()
// 阻止默认事件
// 实现一个最多输入6位数的demo
<input type="text" id="inputText">
<script>
  inputText.oninput = function (e) {
    const ev = e || window.event
    // 去除空格
    const val = this.value.replace(/^ +| +$/g, '')
    const len = val.length
    if(len >= 6) {
      this.value = val.substr(0, 6)
      let code = ev.which || ev.keyCode
      // 排除DELETE/BACK/SPACE/方向键...
      if (!/^(46|8|37|38|39|40)$/.test(code)) {
        ev.preventDefault()
      }
    }
  }
</script>
  • event.stopPropagation()
  • event.stopImmediatePropagation()
这两个都是组织冒泡;区别在于stopImmediatePropagation还能阻止别监听的其他函数不触发
// 实现一个表单验证的demo
<form action="javascript:">
  <input type="text" id="name">
  <input type="text" id="password">
  <button id="submit">提交</button>
</form>
<script>
  const name = document.getElementById('name')
  const password = document.getElementById('password')
  // 输入检查
  submit.addEventListener('click', e => {
    console.log(name.value)
    if (!name.value.trim()) {
      alert('you must input name')
      return e.stopImmediatePropagation()
    }
    if (!password.value.trim()) {
      alert('you must input pwd')
      return e.stopImmediatePropagation()
    }
  }, false)
  // 提交
  submit.addEventListener('click', function () {
    alert('done finish')
  })
</script>
// 可以有效的将检查逻辑和提交代码逻辑进行解耦
提问:在目前主流框架中改如何使用,比如vue、react?
  • event.currentTarget
  • event.current
event.currentTarget始终是事件的监听者/event.current事件的发起者
// 事件委托/事件代理
<ul>
  <li><span>1</span><span>删除</span></li>
  <li><span>2</span><span>删除</span></li>
  <li><span>3</span><span>删除</span></li>
  <li><span>4</span><span>删除</span></li>
  <li><span>5</span><span>删除</span></li>
</ul>
<script>
  // 方案一:递归
  const ul = document.querySelector('ul')
  ul.addEventListener('click', e => {
      console.log(e.currentTarget) // 始终是监听者: ul
      console.log(e.target) // 事件发起者: li 或 span 或 span
      e.stopPropagation()
      remove(ul, e.target)
  })
  function remove(parent, target) {
    if (target.parentElement === parent) {
      parent.removeChild(target)
    } else {
      remove(parent, target.parentNode)
    }
  }
  // 方案二:循环 不用担心函数调用栈溢出
  const ul = document.querySelector('ul')
  ul.addEventListener('click', e => {
    console.log(e.currentTarget) // 始终是监听者: ul
    console.log(e.target) // 事件发起者: li 或 span 或 span
    e.stopPropagation()
    let target = e.target
    while (target.parentElement !== ul){
      target = target.parentElement
    }
    ul.removeChild(target)
  })
</script>

事件代理的优势:

  1. 减少内存消耗,提高性能
  2. 动态绑定事件;例如:当删、增dom元素时,无需再次为子元素绑定事件

提问:在目前主流框架中改如何使用,比如vue、react?

五、自定义事件

// 实现一个游戏启动场景demo
<button>开始游戏</button>
<script>
  // main.js
  const btn = document.querySelector('button')
  btn.addEventListener('click', function() {
      document.dispatchEvent(new Event('gameStart'))
      console.log('开始加载图片资源...')
      console.log('开始加载音频资源...')
  })
  // load-image.js
  document.addEventListener('gameStart', function () {
    setTimeout(() => {
      document.dispatchEvent(new Event('imageLoaded'))
    }, 500)
  })
  // load-music.js
  document.addEventListener('gameStart', function() {
      setTimeout(() => {
        document.dispatchEvent(new Event('musicLoaded'))
      }, 1000)
  })
  // init-scene.js
  document.addEventListener('imageLoaded', () => {
    console.log('图片加载完毕!')
    console.log('创建场景...')
  })
  document.addEventListener('musicLoaded', () => {
    console.log('音频加载完毕!')
    console.log('创建音效...')
  })
</script>
优势:各模块解耦,利于分工,便于扩展
缺点:要杜绝模块间的相互嵌套调用,否则定位问题难,建议由控制模块统一调度

六、函数防抖与节流

函数防抖(debounce)
n秒之后才执行;如果n秒内函数再次触发函数,则重置时间,触发时间不固定。
  // 实现一个输入框检查demo
  <input type="text" id="inpText">
  <script>
    inpText.addEventListener('keyup', check())
    function check() {
      let timer
      return function() {
          clearTimeout(timer)
          timer = setTimeout(() => {
            console.log('check something...')
          }, 800)
      }
    }
  </script>
函数节流(throttle)
立刻执行;如果n秒内函数再次触发函数,则不会重置时间,触发时间固定。
  <section class="wrap">
    <div class="content">xxx</div>
  </section>
  <style>
    .wrap {
      width: 200px;
      height: 400px;
      overflow: scroll;
    }
    .content {
      height: 200%;
      background: aqua;
    }
  </style>
  <script>
    const wrap = document.querySelector('.wrap')
    wrap.addEventListener('scroll', throttle())
    function throttle(e) {
      let canUse = true
      return function () {
        if (!canUse) return
        canUse = false
        if (this.scrollTop - this.clientHeight <= 100) {
          console.log('scroll to end')
        }
        setTimeout(() => {
          canUse = true
        }, 2000)
      }
    }
  </script>

小结

都是为了解决在一定时间内频繁调用js函数的问题,一般情况都和事件相关,关于他们的封装lodash已经做得很好了。

  1. 防抖(debounce)常见的场景:连续的事件,只需触发一次回调。

    • input输入表单验证
    • 搜索框搜索输入;只需用户最后一次输入完,再发送请求
    • 窗口大小Resize。只需窗口调整完成后,计算窗口大小。防止重复渲染。
  2. 节流(throttle)常见的场景:间隔一段时间执行一次回调。

    • 滚动加载,加载更多或滚到底部监听
    • 谷歌搜索框,搜索联想功能
    • 高频点击提交,表单重复提交

七、DOM事件与线程相关内容

  • 待完善

参考资料


JTR354
21 声望1 粉丝

读书点亮生活