认识事件(Event)

Web页面需要经常和用户之间进行交互,而交互的过程中我们可能想要捕捉这个交互的过程:

  • 比如用户点击了某个按钮、用户在输入框里面输入了某个文本、用户鼠标经过了某个位置;
  • 浏览器需要搭建一条JavaScript代码和事件之间的桥梁;
  • 当某个事件发生时,让JavaScript可以相应(执行某个函数),所以我们需要针对事件编写处理程序(handler);

如何进行事件监听呢?

  • 事件监听方式一:在script中直接监听(很少使用);
  • 事件监听方式二:DOM属性,通过元素的on来监听事件;
  • 事件监听方式三:通过EventTarget中的addEventListener来监听;

  <!-- 直接在 html 中编写 JavaScript 代码 -->
  <button onclick="console.log('按钮1发生了点击')">按钮1</button>
  <button class="btn2">按钮2</button>
  <button class="btn3">按钮3</button>
  
  <script>
    // 1.获取元素对象
    var btn2El = document.querySelector('.btn2')
    var btn3El = document.querySelector('.btn3')

    // 2.onclick属性
    /*
        function handleClick01 () {
          console.log('按钮2发生了点击')
        }
        function handleClick02 () {
          console.log('按钮2第二个处理函数')
        }
        btn2El.onclick = handleClick01
        btn2El.onclick = handleClick02
    */

    // 3.
    btn3El.addEventListener('click', function () {
      console.log('按钮3发生了点击')
    })

    btn3El.addEventListener('click', function () {
      console.log('按钮3第二个处理函数')
    })

    btn3El.addEventListener('click', function () {
      console.log('按钮3第三个处理函数')
    })
  </script>

常见的事件列表

鼠标事件:

  • click —— 当鼠标点击一个元素时(触摸屏设备会在点击时生成)。
  • mouseover / mouseout —— 当鼠标指针移入/离开一个元素时。
  • mousedown / mouseup —— 当在元素上按下/释放鼠标按钮时。
  • mousemove —— 当鼠标移动时。

键盘事件:

  • keydown 和 keyup —— 当按下和松开一个按键时。

表单(form)元素事件:

  • submit —— 当访问者提交了一个 <form> 时。
  • focus —— 当访问者聚焦于一个元素时,例如聚焦于一个 <input>

Document 事件:

  • DOMContentLoaded —— 当 HTML 的加载和处理均完成,DOM 被完全构建完成时。
  • CSS 事件:
  • transitionend —— 当一个 CSS 动画完成时。

认识事件流

事实上对于事件有一个概念叫做事件流,为什么会产生事件流呢?

  • 我们可以想到一个问题:当我们在浏览器上对着一个元素点击时,你点击的不仅仅是这个元素本身;
  • 这是因为我们的HTML元素是存在父子元素叠加层级的;
  • 比如一个span元素是放在div元素上的,div元素是放在body元素上的,body元素是放在html元素上的

事件冒泡和事件捕获

我们会发现默认情况下事件是从最内层的span向外依次传递的顺序,这个顺序我们称之为事件冒泡(Event Bubble);

事实上,还有另外一种监听事件流的方式就是从外层到内层(body -> span),这种称之为事件捕获(Event Capture);

为什么会产生两种不同的处理流呢?

  • 这是因为早期浏览器开发时,不管是IE还是Netscape公司都发现了这个问题;
  • 但是他们采用了完全相反的事件流来对事件进行了传递;
  • IE采用了事件冒泡的方式,Netscape采用了事件捕获的方式;

那么我们如何去监听事件捕获的过程呢

事件捕获和冒泡的过程

如果我们都监听,那么会按照如下顺序来执行:

捕获阶段(Capturing phase):

  • 事件(从 Window)向下走近元素。

目标阶段(Target phase):

  • 事件到达目标元素。

冒泡阶段(Bubbling phase):

  • 事件从元素上开始冒泡。

事实上,我们可以通过event对象来获取当前的阶段:

  • eventPhase

开发中通常会使用事件冒泡,所以事件捕获了解即可。

事件对象

当一个事件发生时,就会有和这个事件相关的很多信息:

  • 比如事件的类型是什么,你点击的是哪一个元素,点击的位置是哪里等等相关的信息;
  • 那么这些信息会被封装到一个Event对象中,这个对象由浏览器创建,称之为event对象;
  • 该对象给我们提供了想要的一些属性,以及可以通过该对象进行某些操作;

如何获取这个event对象呢?

  • event对象会在传入的事件处理(event handler)函数回调时,被系统传入;
  • 我们可以在回调函数中拿到这个event对象;

这个对象中都有哪些常见的属性和操作呢?

event常见的属性和方法

常见的属性:

  • type:事件的类型;
  • target:当前事件发生的元素;
  • currentTarget:当前处理事件的元素;
  • eventPhase:事件所处的阶段;
  • offsetX、offsetY:事件发生在元素内的位置;
  • clientX、clientY:事件发生在客户端内的位置;
  • pageX、pageY:事件发生在客户端相对于document的位置;
  • screenX、screenY:事件发生相对于屏幕的位置;
  <div class="box">
    <span class="btn">
      <button>按钮</button>
    </span>
  </div>
  <br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br>
  
  <script>
    var divEl = document.querySelector('div')
    var btnEl = document.querySelector('.btn')

    /* 
      btnEl.onclick = function () {
      console.log('按钮发生了点击~')
     } */

    divEl.onclick = function (event) {
      // 1.偶尔使用
      console.log('事件类型:', event.type)
      console.log('事件阶段:', event.eventPhase)

      // 2.了解
      console.log('事件元素中位置:', event.offsetX, event.offsetY)
      console.log('事件客户端中位置:', event.clientX, event.clientY)
      console.log('事件发生在页面位置:', event.pageX, event.pageY)
      console.log('事件发生在屏幕位置:', event.screenX, event.screenY)

      // 3.target/currentTarget
      console.log(event.target) // 事件发生的对象
      console.log(event.currentTarget) // 事件处理的对象
      console.log(event.target === event.currentTarget)
    }
  </script>

常见的方法:

  • preventDefault:取消事件的默认行为;
  • stopPropagation:阻止事件的进一步传递(冒泡或者捕获都可以阻止);
  <a href="https://www.baidu.com">google一下</a>

  <div class="box">
    <span>
      <button>按钮</button>
    </span>
  </div>

  <script>
    /* 1.阻止默认行为
    var aEl = document.querySelector('a')
    aEl.onclick = function (event) {
      console.log('a元素发生了点击')
      event.preventDefault()
    }
    */

    // 2.阻止事件进一步传递
    var btnEl = document.querySelector('button')
    var spanEl = document.querySelector('span')
    var divEl = document.querySelector('div')

    divEl.addEventListener('click', function (event) {
      console.log('div 的事件捕获监听~')
      // event.stopPropagation()
    }, true)

    spanEl.addEventListener('click', function () {
      console.log('span 的事件捕获监听~')
    }, true)

    btnEl.addEventListener('click', function (event) {
      console.log('button 的事件捕获监听~')
      // event.stopPropagation()
    }, true)

    divEl.addEventListener('click', function () {
      console.log('div 的事件冒泡监听~')
    })

    spanEl.addEventListener('click', function (event) {
      console.log('span 的事件冒泡监听~')
      event.stopPropagation()
    })

    btnEl.addEventListener('click', function () {
      console.log('button 的事件冒泡监听~')
    })
  </script>

事件处理函数中的this

在函数中,我们也可以通过this来获取当前的发生元素:

这是因为在浏览器内部,调用event handler是绑定到当前的currentTarget上的

  <div>
    <button>按钮</button>
  </div>

  <script>
    var btnEl = document.querySelector('button')
    var divEl = document.querySelector('div')

    divEl.onclick = function (event) {
      console.log(this)
      console.log(event.currentTarget)
      console.log(divEl)
      console.log(this === divEl)
    }

    /*
        divEl.addEventListener('click', function () {
          console.log(this)
        })
     */
    /*
      var fn = btnEl.onclick
      fn() // 默认调用 window
      btnEl.onclick() // this -> btnEl
    */

EventTarget类

我们会发现,所有的节点、元素都继承自EventTarget

  • 事实上Window也继承自EventTarget;

那么这个EventTarget是什么呢?

  • EventTarget是一个DOM接口,主要用于添加、删除、派发Event事件;

EventTarget常见的方法:

  • addEventListener:注册某个事件类型以及事件处理函数;
  • removeEventListener:移除某个事件类型以及事件处理函数;
  • dispatchEvent:派发某个事件类型到EventTarget上;

    window.addEventListener('what', function () {
      console.log('监听到what?')
    })

    setTimeout(function () {
      window.dispatchEvent(new Event('what'))
    }, 500)

事件委托(event delegation)

事件冒泡在某种情况下可以帮助我们实现强大的事件处理模式 – 事件委托模式(也是一种设计模式)

那么这个模式是怎么样的呢?

  • 因为当子元素被点击时,父元素可以通过冒泡可以监听到子元素的点击;
  • 并且可以通过event.target获取到当前监听的元素;

案例:一个ul中存放多个li,点击某一个li会变成红色

  • 方案一:监听每一个li的点击,并且做出相应;
  • 方案二:在ul中监听点击,并且通过event.target拿到对应的li进行处理;
    • 因为这种方案并不需要遍历后给每一个li上添加事件监听,所以它更加高效;

<ul>
    <li>1</li>
    <li>2</li>
    <li>3</li>
    <li>4</li>
    <li>5</li>
    <li>6</li>
    <li>7</li>
    <li>8</li>
    <li>9</li>
    <li>10</li>
  </ul>

  <script>
    /* 1.每一个li都监听自己的点击,并且有自己的处理函数
      var liEls = document.querySelectorAll('li')
      for (var liEl of liEls) {
        // 监听点击
        liEl.onclick = function (event) {
          // this.classList.add('active')
          event.currentTarget.classList.add('active')
        }
      }
    */
    
    /* 2.统一在 ul 中监听
      var ulEl = document.querySelector('ul')
      ulEl.onclick = function (event) {
        console.log('点击了某一个li', event.target)
        event.target.classList.add('active')
      }
    */

    // 3.点击 li 变成 active,其他的取消
    var ulEl = document.querySelector('ul')
    var activeLiEl = null
    ulEl.onclick = function (event) {
      // 1.将之前的active移除
      /* for (var i = 0; i < ulEl.children.length; i++) {
        var liEl = ulEl.children[i]
        if (liEl.classList.contains('active')) {
          ulEl.children[i].classList.remove('active')
        }
      } */

      // 找到active的li,移除掉active
      /* var activeLiEl = ulEl.querySelector('.active')
      activeLiEl && activeLiEl.classList.remove('active') */

      // 变量记录的方式
      if (activeLiEl) {
        activeLiEl.classList.remove('active')
      }

      // 2.给点击的元素添加active
      event.target.classList.add('active')
      console.log(event.target)
      // 3.记录最新的active对应的li
      activeLiEl = event.target
    }
 <div class="box">
    <button data-action="remove">移除</button>
    <button data-action="new">新建</button>
    <button data-action="search">搜索</button>
    <button>11111111</button>
  </div>

  <script>
    var boxEl = document.querySelector('.box')
    boxEl.onclick = function (event) {
      var btnEl = event.target
      var action = btnEl.dataset.action
      switch (action) {
        case 'remove':
          console.log('点击了移除按钮')
          break
        case 'new':
          console.log('点击了新建按钮')
          break
        case 'search':
          console.log('点击了搜索按钮')
          break
        default:
          console.log('点击了其他')
      }
    }
  </script>

事件委托的标记

某些事件委托可能需要对具体的子组件进行区分,这个时候我们可以使用data-*对其进行标记:

比如多个按钮的点击,区分点击了哪一个按钮:

常见的鼠标事件

接下来我们来看一下常见的鼠标事件(不仅仅是鼠标设备,也包括模拟鼠标的设备,比如手机、平板电脑)

常见的鼠标事件:

 <div class="box"></div>
  <script>
    // 鼠标事件
    var boxEl = document.querySelector('.box')

    boxEl.onclick = function () {
      console.log('box')
    }

    boxEl.oncontextmenu = function (event) {
      console.log('点击了右键')
      event.preventDefault()
    }

    var isDown = false
    boxEl.onmousedown = function () {
      console.log('鼠标按下去')
      isDown = true
    }
    
    boxEl.onmouseup = function () {
      console.log('鼠标抬起来')
      isDown = false
    }

    boxEl.onmousemove = function () {
      if (isDown) {
        console.log('鼠标在div上移动')
      }
    }
  </script>

mouseover和mouseenter的区别

mouseenter和mouseleave

  • 不支持冒泡
  • 进入子元素依然属于在该元素内,没有任何反应

mouseover和mouseout

  • 支持冒泡
  • 进入元素的子元素时
    • 先调用父元素的mouseout
    • 再调用子元素的mouseover
    • 因为支持冒泡,所以会将mouseover传递到父元素中;

<div class="box">  
 <button>删除</button>  
 <button>新增</button>  
 <button>搜索</button>  
  
 <script>  
 // 方案一: 监听的本身就是button元素  
 /*   var btnEls = document.querySelectorAll('button')  
  for (var btnEl of btnEls){  btnEl.onmouseenter = function(event){  console.log(event.target.textContent)  }  } */  
 //  方案二: 事件委托  
 var boxEl = document.querySelector('.box')  
 boxEl.onmouseover = function(event){  
  if (event.target.tagName==='BUTTON'){  
   console.log(event.target.textContent)  
  } } </script>

常见的键盘事件

常见的键盘事件:

事件的执行顺序是 onkeydown、onkeypress、onkeyup

  • down事件先发生;
  • press发生在文本被输入;
  • up发生在文本输入完成(抬起/松开);

我们可以通过key和code来区分按下的键:

  • code:“按键代码”(“KeyA”,“ArrowLeft” 等),特定于键盘上按键的物理位置。
  • key:字符(“A”,“a” 等),对于非字符(non-character)的按键,通常具有与 code 相同的值。
<input type="text">  
<input class="input2" type="text">  
<button>搜索</button>  
<script>  
var inputEl = document.querySelector('input')  
var btnEl = document.querySelector('button')  
  
/* inputEl.onkeydown = function() {  
 console.log('onkeydown') } inputEl.onkeypress = function() { console.log('onkeypress') } inputEl.onkeyup = function(event) { console.log('onkeyup',event.key,event.code) } */  
// 搜索功能  
btnEl.onclick = function() {  
 console.log('进行搜索功能', inputEl.value)  
}  
  
inputEl.onkeyup = function(event) {  
 if (event.key === 'Enter') {  
  console.log('进行搜索功能', inputEl.value)  
 }}  
  
// 按下s自动获取焦点  
document.onkeyup = function(event) {  
 console.log('document键盘事件', event.key, event.code)  
 if (event.code === 'KeyS') {  
  console.log('用户点击了s')  
  inputEl.focus()  
 }}  
</script>

常见的表单事件

针对表单也有常见的事件:

<form action="/abc">  
 <input type="text">  
 <textarea name="" id="" cols="30" rows="10"></textarea>  
 <button type="reset">重置</button>  
 <button type="submit">提交</button>  
</form>  
  
<script>  
var inputEl = document.querySelector('input')  
  
// 1.获取焦点和失去焦点  
/*  
inputEl.onfocus = function(){  
 console.log('input获取到了焦点')  
}  
inputEl.onblur = function(){  
 console.log('input失去了焦点')  
}  
*/  
  
// 2.内容发生改变/输入内容  
// 输入的过程: input  
// 内容确定发生改变(离开): change  
/* inputEl.oninput = function(){  
 console.log('oninput事件-----正在输入内容',inputEl.value)  
} */  
/* inputEl.onchange = function(){  
 console.log('onchange事件-----内容发生了改变',inputEl.value)  
} */  
  
// 3.监听重置和提交  
var formEl = document.querySelector('form')  
formEl.onreset = function(event) {  
 console.log('发生了重置事件')  
  
}  
formEl.onsubmit = function(event) {  
 console.log('onsubmit 事件')  
 event.preventDefault()  
}  
</script>

文档加载事件

DOMContentLoaded:浏览器已完全加载 HTML,并构建了 DOM 树,但像 和样式表之类的外部资源可能尚未加载完成。

load:浏览器不仅加载完成了 HTML,还加载完成了所有外部资源:图片,样式等。

<script>  
  
// 注册事件监听  
window.addEventListener('DOMContentLoaded', function() {  
 // 1.这里可以操作box,box已经加载完毕  
 /*   var boxEl = document.querySelector('.box')  
  boxEl.style.backgroundColor = 'orange'  console.log('HTML内容加载完毕') */  
  
 //   2.获取img对应的图片宽高  
 var imgEl = document.querySelector('img')  
 console.log('img的宽高:',imgEl.style.width,imgEl.style.height)  
})  
  
window.onload = function() {  
 var imgEl = document.querySelector('img')  
 console.log('img的宽高:',imgEl.offsetWidth,imgEl.offsetWidth)  
 console.log('文档中所有资源加载完毕')  
}  
  
window.onresize = function(){  
 console.log('窗口大小发生改变')  
}  
</script>  
  
<div class="box">  
 <p>呵呵呵呵</p>  
</div>  
<a href="#">google一下</a>  
<img src="./clock.jpg" alt="">

事件类型:https://developer.mozilla.org/zh-CN/docs/Web/Events

Q.E.D.