浏览器事件面试点总结

1,536 阅读13分钟

浏览器事件

常见的浏览器事件

  1. 鼠标事件
  • click 鼠标点击触发
  • mousemove 鼠标移动时触发
  • mouseleave 鼠标离开某元素时触发
  • mouseenter/mouseout 鼠标移进移出事件
  • mousedown/mouseup 按下/释放鼠标事件
  • contextmenu 鼠标右键点击事件

mouseleave & mouseout 的差别 两者同是鼠标移开事件, 不同点是 mouseout 的触发对象不仅是目标元素还包括目标元素的子元素, 而 mouseleave 只在鼠标理开目标元素(把子元素当作目标元素的一部分)之后离开。

  1. 键盘事件
  • keydown/keydup 键盘按下松开事件
  1. DOM 事件
  • copy
  • cut
  • scorll
  • load
  • resize
  1. 表单事件
  • submit
  • reset

添加事件处理

html 行内添加

使用元素的 on<event> 属性添加事件。

<button onclick="alert(1)"></button>

或者

<button onclick="handleClick()"></button>
<script>
function handleClick() {
    alert(1)
}
</script>  

JS DOM 调用

<button id="btn" onclick="handleClick()"></button>
<script>
const btn = document.getElementById('btn')
btn.onclick = function() {
    alert(1)
}
</script>  

注意事项

  1. this 指向 事件处理程序中的 this 指向对应的元素
<button onclick="alert(this.innerHTML)">click button</button>

上面的代码中 this.innerHTML 显示元素的内容

  1. 程序分配 & 程序执行

当使用 js DOM 调用时,函数应该被分配给 DOM.on<event>, 而不是执行。

// 正确
button.onclick = sayThanks;

// 错误
button.onclick = sayThanks();

当作为 html 的 on<event> 属性时,浏览器会创建一个处理程序,在这个处理程序中希望执行一些代码。

<button onclick="sayThanks()">click button</button>

on<event> 模式弊端

当需要在元素上为一个事件分配两个事件处理是不被允许的。新的旧的处理程序总是会被覆盖。

<button id="btn">click btn</button>
<script>
const btn = document.getElementById('btn')
btn.onclick = function1(){}
btn.onclick = function2(){}
</script>  

以上代码只执行 function1

addEventListener 事件注册

该方法将指定的监听注册器注册到目标元素上, 当该元素触发指定的事件时,指定的回调函数就会被执行。

使用方法

ele.addEventListener(event, handler[, options])

event 事件名称

handler 处理程序

options 附加选项

  • once 如果为 true , 那么事件触发后将会自动删除监听器
  • capture 事件处理阶段, [事件冒泡/捕获]
  • passive 如果为 truepreventDefault() 将不会生效
<button id='btn'>btn</button>
<a href="" id='abtn'>demo</a>
<script>
    let btn = document.getElementById('btn')
    let abtn = document.getElementById('abtn')
   
    abtn.addEventListener('click', function(e){
        e.preventDefault()
        console.log(e, this)
    }, {passive: false}) // 阻止 a 标签的默认跳转行为

</script>

对于上面使用 on<event> 形式不能在一个元素上面同时绑定两个事件的问题, addEventListener 可以很好的解决。

    btn.addEventListener('click', function(e){
        console.log('fn1')
    }, false) 
     btn.addEventListener('click', function(e){
        console.log('fn2')
    }, false) 

上面代码这个事件都将会执行。

removeEventListener 移除事件注册

element.removeEventListener(event, handler[, options]) removeEventListener 函数中参数必须同 addEventlistener 中保持一致。

handler 保持一致

为了使 handler 处理程序在 addEventListenerremoveEventListener 中保持一致, 需要把函数储存在一个变量中。

function handler() {
    console.log('handler')
}

btn.addEventListener('click', handler)
btn.removeEventListener('click', handler)

如果不使用这种方式事件将不能移除 下面的代码不会起作用。

elem.addEventListener( "click" , () => alert('Thanks!'));
// ....
elem.removeEventListener( "click", () => alert('Thanks!'));

event 对象

无论使用 on<event> 还是使用 addEventListener 绑定事件,在处理函数中有都会有一个 event 事件对象参数。

btn.onclick = function(event){
    console.log(event)
}
btn.addEventListener('click', function(event){
    console.log(event)
})
// MosueEvent {......}

event 对象常用的属性

  • event.type 事件类型的名称, 如 click mouseover
  • event.target 触发事件的元素。
  • event.currentTarget 触发事件的当前元素。
  • event.clientX / event.clientY 鼠标相对于窗口的 x/y 轴坐标

事件冒泡和捕获

DOM 事件执行的3个阶段:

1. 捕获阶段     自上向下
2. 执行阶段
3. 冒泡阶段     自下向上

冒泡

当一个事件发生时, 会首先运行该元素上的处理程序, 然后运行其父级元素上的处理程序,然后一直向上到其祖元素(documentwindow)上的处理程序。 是一个事件从下到上的事件穿透过程。

<form onclick="alert('form')">FORM
  <div onclick="alert('div')">DIV
    <p onclick="alert('p')">P</p>
  </div>
</form>

当点击 p 元素的时候会分别有3个 alert 执行,顺序为 p -> div -> form

event.target & event.currentTarget

上一节讲到 handler 函数中的 event 对象包含了 targetcurrentTarget 两个属性, 他们在冒泡过程中存在区别。

改写一下上面的例子

<form id='form'>FORM
  <div id='div'>DIV
    <p id="p">P</p>
  </div>
</form>
<script>
    const form = document.getElementById('form')
    const div = document.getElementById('div')
    const p = document.getElementById('p')
    function handler(event) {
        console.log(event.target, event.currentTarget)
    }
    form.addEventListener('click', handler)  
    div.addEventListener('click', handler)
    p.addEventListener('click', handler)

</script>

当点击 p 元素时, 输出结果: p 元素 / P 元素 -> p 元素 / div 元素 -> p 元素 / from 元素

可以看出

  1. event.target 是引发事件的目标元素, 在事件冒泡的过程中并不会发生改变。
  2. event.currentTarget 表示当前正在执行事件的元素(冒泡到该元素)。

停止冒泡

事件冒泡从目标元素开始,会一直上升到顶级元素 document, 有些事件也可以到达 window, 这个过程要调用路径上所有的处理程序。 这个路径上任何一个元素都可以使用 event.stopPropagation() 停止冒泡。

<form onclick="alert('form')">FORM
  <div onclick="alert('div')">DIV
    <p onclick="event.stopPropagation()">P</p>
  </div>
</form>

当使用 addEventListener 为一个元素添加多个事件程序时候, 即使阻止了其中一个事件冒泡, 其他的处理程序依旧会依序执行造成冒泡, 使用 event.stopImmediatePropagation() 用于停止冒泡,并阻止当前元素上其他的处理程序运行。

捕获

事件执行需要经过三个阶段: 1.事件捕获。 2. 事件执行。 3.事件冒泡。 实际上事件在执行前都会经历过一个事件捕获阶段。这个阶段从 window 开始一直向下穿透捕获到目标元素,从而给元素执行事件程序,但是默认情况下浏览器执行事件时已经忽略了事件捕获阶段,事件仅在第二阶段和第三阶段执行。

开启事件捕获阶段

addEventListener 函数中,将 capture 参数设置为 true

btn.addEventListener('click', handler, {capture: true})

例子:

<form>FORM
  <div>DIV
    <p>P</p>
  </div>
</form>

<script>
  for(let elem of document.querySelectorAll('*')) {
    elem.addEventListener("click", e => alert(`Capturing: ${elem.tagName}, ${e.eventPhase}`), true);
    elem.addEventListener("click", e => alert(`Bubbling: ${elem.tagName}, ${e.eventPhase}`));
  }
</script>

代码中为 form 元素及子元素都注册了 捕获事件和一般事件,执行顺序为

  1. HTML -> BODY -> FROM -> DIV -> P (捕获阶段)
  2. P -> DIV -> FORM -> BODY -> HTML (冒泡阶段)

在事件过程中可以使用 event.eventPhase 获取到当前执行事件所处的事件执行阶段。

事件委托

事件的冒泡和捕获提供了事件委托模式。 这种模式适用于:需要为多个元素添加类似的处理事件,为了避免给每个元素绑定事件,可以将处理事件放到这些元素的共同父元素上面。

普通事件委托

如下代码,需要给表格中的每个 <td> 标签添加一个点击事件,使得该单元格背景发生改变。

<table>
  <tr>
    <th colspan="3"><em>Bagua</em> Chart: Direction, Element, Color, Meaning</th>
  </tr>
  <tr>
    <td class="nw"><strong>Northwest</strong><br>Metal<br>Silver<br>Elders</td>
    <td class="nw"><strong>Northwest</strong><br>Metal<br>Silver<br>Elders</td>
    <td class="nw"><strong>Northwest</strong><br>Metal<br>Silver<br>Elders</td>
  </tr>
  <tr>
    <td class="nw"><strong>Northwest</strong><br>Metal<br>Silver<br>Elders</td>
    <td class="nw"><strong>Northwest</strong><br>Metal<br>Silver<br>Elders</td>
    <td class="nw"><strong>Northwest</strong><br>Metal<br>Silver<br>Elders</td>
  </tr>
  <tr>
    <td class="nw"><strong>Northwest</strong><br>Metal<br>Silver<br>Elders</td>
    <td class="nw"><strong>Northwest</strong><br>Metal<br>Silver<br>Elders</td>
    <td class="nw"><strong>Northwest</strong><br>Metal<br>Silver<br>Elders</td>
  </tr>
</table>

这时候可以把点击事件绑定到 <table> 上面, 他将在处理程序中使用 event.target 获取到执行事件的元素。

let selected  // 用来存放当前选择的 td 元素 

table.addEventListener('click', function(event){
    const target = event.target

    if(taget.tagName !== 'TD') return  //忽略不在 td 上面的点击

    if(selected) {
        selected.style.backgroundColor = "transparent"
    }

    selected = target
    selected.style.backgroundColor = "red"
})

上面的代码在一定程度上实现了要求,但是当鼠标点击到 <td> 中的 <strong> 标签时, 由于 event.target.tagName 此时并不是 TD 所以会失效。这是可以使用 event.target.closest('tagname') 获取到距离 tagname 标签最近的元素。 修改后的代码为:

let selected  // 用来存放当前选择的 td 元素 

table.addEventListener('click', function(event){
    const td = event.target.closest('td')
    if(!td) return  // 忽略找不到 td 的元素点击
    if(!table.contains(td)) return  // 忽略不在 table 当中的 td 元素 

    if(selected) {
        selected.style.backgroundColor = "transparent"
    }

    selected = target
    selected.style.backgroundColor = "red"
})
  1. 使用 event.target.closest(tagname) 返回距离 tagname 的元素
  2. 使用 ele.contains(eleChild) 判断 ele 元素中是否包含 eleChild 元素

使用 data- 实现标记委托

如下所示, 存在一个按钮组, 需要给每个按钮添加不同的事件。 这时可以使用 data- 给每个按钮添加一个 action 标记。

<div id="menu">
    <button data-action="save">保存</button>
    <button data-action="fresh">刷新</button>
    <button data-action="search">搜索</button>
</div>

使用事件委托, 把事件绑定到 menu 上用来捕获内部 button 的点击。

const menu = document.getElementById('menu')
function save() {
    console.log('save')
}
function fresh() {
    console.log('fresh')
}
function search() {
    console.log('search')
}
menu.addEventListener('click', (event) => {
    const target = event.target.dataset.action
    if(!target) return
    this[target]()
})

在第一节中,讲到在 handler 处理程序中, this 指向当前元素, 但是这里我们使用箭头函数,改变函数里面的 this 指向 window, 这是就可以通过 this[target]() 调用我们在全局定义的方法。

浏览器的默认行为

对于许多元素浏览器赋予了默认功能, 如 <a> 标签点击默认实现路径跳转、表单中的提交按钮点击触发提交行为。这些默认行为并不是符合所有的业务场景的, 这时就需要阻止浏览器的默认行为。

阻止浏览器行为

有两种是实现方式:

  1. 使用 event.preventDefault() 方法。
  2. 使用 on<event> 模式指定事件 return false
<a onclick="return false">click</a>
<a onclick="event.preventDefault()">click</a>

event.preventDefault() 方法并不是总是执行的, 还记得在 addEventListener 中的第三个参数 optionspassive 用于设置 event.preventDefault() 是否生效

<a href="https://www.baidu.com" id='aLink'>click</a>

<script>
const aLink = document.getElementById('aLink')
aLink.addEventListener('click', event => {}, {passive: true})
</script>  

上面的代码因为在 addEventListener 的第三个参数中设置了 passive: true 导致了 event.preventDefault() 并不能生效

自定义鼠标右击事件

浏览器给鼠标右击默认添加了事件,可以通过 event.preventDefault() 阻止这个默认事件以便自定义右击事件。

<div oncontextmenu="alert(1)">
    <button id='btn'></button>
</div>

<script>
    const btn = document.getElementById('btn')
    btn.addEventListener('contextmenu', event => {
        event.preventDefault()
        alert(1)
    })
</script>

btn 元素上绑定了一个 contextmenu 事件, 使用 event.preventDefault() 阻止了浏览器的默认事件, 但是因为事件冒泡右击事件在 btn 元素上执行完毕后又冒泡到了 div 元素上,所以这里也需要使用 event.stopPropagation() 阻止事件冒泡。

    const btn = document.getElementById('btn')
    btn.addEventListener('contextmenu', event => {
        event.preventDefault()
        event.stopPropagation()
        alert(1)
    })

自定义事件

元素上不仅可以分配浏览器内置的事件, 还可以自定义事件分配到元素上。

自定义事件构成

自定义事件需要两个步骤, 第一步是通过 Event 构造器生成一个自定事件, 第二步是在需要的地方通过 dispatchEvent 调用事件。

Event 构造器

let event = new Event(type[, options])
  • type 事件类型, 像 click 这样的字符串
  • options 事件参数
    • bubbles -true/false 该事件是否冒泡。 true 为冒泡。
    • cancelable - true/false 该事件是否阻止浏览器默认行为。

默认情况下, bubblescancelable 都为 false

dispatchEvent

自定义的事件并不能像原生事件那样通过 DOM 的操作触发, 只能通过 diapatchEvent(event) 方法手动调用。

<button id="custom">自定义事件</button>
<script>
    const custom = document.getElementById('custom')
    custom.addEventListener('myEvent', event => {
        console.log(event)
    })

    const event = new Event('myEvent')
    custom.dispatchEvent(event)
</script>

上面的代码包含了自定义事件执行的三个步骤。

  1. 在元素上注册了一个事件监听
  2. 生成一个 Event 事件实例
  3. 通过 dispatchEvent 调用

自定义事件的冒泡

在上述的代码中自定义事件的调用和事件监听都是通过一个元素进行的, 如果希望父级元素通过冒泡的机制监听事件,可以通过设置 Event 构造器的第二个参数 bubbles: true

<button id="custom">自定义事件</button>
<script>
    const custom = document.getElementById('custom')
    document.addEventListener('myEvent', event => {
        console.log(event)
    })

    const event = new Event('myEvent', {
        bubbles: true
    })
    custom.dispatchEvent(event)
</script>

值得注意的是, 自定义事件必须通过 addEventListener 注册监听, 并不能通过 on<event> 的方式调用, 因为 on<event> 只能调用内建事件,ele.onMyEvent 并不会执行。

UI 规范事件

实际上 Event 构造函数是所有浏览器事件的基类, 浏览器通过它派生出许多其他规范事件类型, 如 UIEvent FocusEvent MouseEvent WheelEvent KeyboardEvent, 这些事件都基于 Event 构造函数添加了各自的属性和方法。通过这些规范事件构造的实例可以添加一些标准属性。

const event = new MouseEvent('myClick', {
    bubbles: true,
    cancelable: true,
    clientX: 120,
    clientY: 200
})

因为 MouseEvent 中包含了 clientXclientY 属性, 这使得在自定义事件中可以初始化这些属性, 而这在通过 Event 构造实例时是不起作用的。

为自定义事件添加附加属性

在大部分情况下, 自定义的事件都应该是一个全新的事件类型, 并且应该使用 new CustomEvent 生成一个事件实例。 这样的好处是不必知道该事件应该属于哪种标准规范事件, 并且 CustomEvent 在第二个参数中允许添加附加属性 detail

<h3 id="h3">Hello Custom Event </h3>

<script>
    const h3 = document.getElementById('h3')

    h3.addEventListener('hello', event => {
        console.log(event.detail.msg)
    })

    h3.dispatchEvent(new CustomEvent('hello', {
        detail: {
            msg: 'hello custom'
        }
    }))
</script>

自定义时间的执行顺序

<button id="menu">Menu (click me)</button>
<script>
    menu.addEventListener('click', () => {
        menu.dispatchEvent(new CustomEvent('asyncEvent'))

        alert(1)
    })

    menu.addEventListener('asyncEvent', () => {
        console.log('2')
    })
</script>

上述代码中由于 在 click 事件程序中使用 dispatchEvent 触发了一个自定义事件, 这时函数的执行会立即跳到 addEventListener 注册的监听程序中,当监听程序的代码执行完毕执行完毕后才会再次回到 click 的处理程序中. 所以上面的执行顺序是 2 -> 1