【前端复习计划】- 事件模型

253 阅读7分钟

浏览器的事件模型,就是通过监听函数对事件触发做出反应,事件发生后,浏览器监听到了这个事件,就会执行对应的监听函数,这就是事件驱动编程模式

事件流程

触发一个绑定在元素上的事件后,会经历以下流程:

  • 捕获阶段: 事件流程从根元素(body)开始,由外到内进行捕获,直到目标元素(绑定事件的这个元素)
  • 目标阶段: 目标阶段就是通过事件捕获,捕捉到了当前事件触发在哪个 DOM 元素上的过程,即目标元素
  • 冒泡阶段: 事件流程经过了目标阶段以后,还会从当前目标元素开始,由内到外的一级一级传递到根元素 body 上,类似于气泡浮出水面的过程

事件流程示意图

上面示意图表达的过程解释如下:

  • 捕获:页面上的HTML是一层层嵌套的,当点击其中任意一个元素时,浏览器需要知道,到底是哪个元素被点击了,所以就会从根节点开始一层层往里捕获节点(1——4)
  • 目标:浏览器定位到了被点击的元素,成为目标阶段(4——5)
  • 冒泡:目标元素开始执行响应事件,但是同理,嵌套着目标元素的父元素同时也被点击了,也要执行它上面绑定的事件(不管有无人为绑定),所以就会从目标元素开始,一层层的往外,最终会到达根节点(5——8)

特定情况下,你只需要目标元素响应事件而要避免它的父元素响应事件时,你就要阻止事件冒泡,使用 stopPropagation() 方法

const element = document.getElementById('app')

element.addEventListener('click', (e) => {
  // 阻止当前触发事件冒泡到当前元素的外层元素
  e.stopPropagation()
  console.log('我被点击了')
})

还有些元素含有默认事件响应(非人为设定的事件响应代码),为了只让目标元素响应事件,你还要阻止默认事件响应,使用 preventDefault()

const element = document.getElementById('app') // 这里 element 是一个 <a> 元素,具有默认的点击后跳转页面的事件

element.addEventListener('click', (e) => {
  // 阻止当前触发事件冒泡到当前元素的外层元素
  e.preventDefault()
  console.log('我将不会进行页面跳转')
})

事件绑定

on

可以直接在 HTML 标签上绑定 Javascript 事件,虽然这种方式非常不推荐使用

<body onload="doSomething()">
  
<div onclick="console.log('触发事件')">
  
<div onmousemove="console.log('触发事件')">

在 HTML 标签上绑定事件都是 on + 【事件名】 的形式,例如上面的 onmousemove 。还需要注意两点:

  • 标签上的 on 事件监听只能传入 Javascript 语句或者函数调用,而不能仅仅是一个函数名称
<!-- 正确 -->
<body onload="doSomething()">

<!-- 错误 -->
<body onload="doSomething">
  • 使用这个方法指定的监听代码,只会在冒泡阶段触发,而且不能控制在哪个阶段来执行
  • 同一个元素,只能绑定一个事件监听函数

addEventListener

这个方法是最常用的事件监听注册方式。不同于 HTML 标签上的事件绑定,addEventlistener 可以为同一个元素绑定多个监听函数

const element = document.getElementById('app')

element.addEventListener('click', () => console.log('我被点击了'))

element.addEventListener('mousemove', () => console.log('发生了鼠标移动'))

使用 addEventListener 绑定的事件监听函数必须通过 removeEventListener 来移除

const element = document.getElementById('app')

const callBack = function(){
  console.log('do something')
}

element.addEventListener('click', callBack)

//  do something ....................

// 移除事件监听
element.removeEventListener('click', callBack)

addEventListener 的标准形式是 element.addEventListener(type, listener, options),它一共有三个参数:

  • type : 触发的事件类型,比如 click、mouserover等等
  • listener: 事件监听所绑定的回调函数,也就是事件被触发后会执行的操作
  • options: options 是一个对象,它有三个属性:
    • capture: 布尔值,指定 listener 是否在事件的捕获阶段触发,默认值为 false
    • once: 布尔值,表示 listener 在添加之后最多只调用一次。如果为 true, listener 会在调用之后自动移除
    • passive: 布尔值,设置为true时,表示 listener 永远不会调用 preventDefault()

第三个参数还有一种写法,那就是只传递一个布尔值 useCapture ,它跟 options.capture 的作用相同,不过,这是过去的写法,不建议使用

下面来详细说一下,options.capture 这个参数的作用,首先有一段程序:

// 样式代码忽略....

<div id="out">
    <h1>out</h1>
    <div id="middle">
        <h1>middle</h1>
        <div id="inside">
            <h1>inside</h1>
            <button id="button">点击我</button>
        </div>
    </div>
</div>

<script>
    const out = document.getElementById('out');
    const middle = document.getElementById('middle');
    const inside = document.getElementById('inside');
    const button = document.getElementById('button');
  
    out.addEventListener('click', () => {console.log("out")}, {
        capture: false
    });
    inside.addEventListener('click', () => {console.log("inside")}, {
        capture: false
    });
    middle.addEventListener('click', () => {console.log("middle")}, {
        capture: false
    });
    button.addEventListener('click', () => {console.log("button")}, {
        capture: false
    });
</script>

前面说过,capture 这个参数默认为 false,首先来看看事件触发的顺序会是怎样:

事件触发的顺序

结合上面的事件流程的图就可以知道,所有的事件都是在冒泡阶段被触发的,从 button 开始一直向外冒泡,触发了所有外层元素的事件

现在把所有的 capture 都设置为 true,再来看看结果会是怎样

事件捕获触发

现在,顺序完全反了过来,从最外层的 out 开始一直往内触发事件,这不就是事件捕获嘛!所以,capture 的参数的作用应该是明了的:指定事件触发的顺序,也就是指定事件是捕获阶段触发,还是在冒泡阶段触发

事件委托

事件委托这个概念就相对简单一些,也是基于上面的事件模型,我们用一个程序来举例子。现在有一个 ul 无序列表,里面有很多项 li ,需要你实现的是:点击每个 li 之后,在控制台打印这个 li 的文本内容

<ul>
	<li>this is li <h1>1</h1></li>
	<li>this is li <h1>2</h1></li>
	<li>this is li <h1>3</h1></li>
	<li>this is li <h1>4</h1></li>
	<li>this is li <h1>5</h1></li>
</ul>

<script type="text/javascript">
	const allLi = document.querySelectorAll('li');

	allLi.forEach(li => {
		li.addEventListener('click',show);
	})
	
	function show(e){
		console.log(e.currentTarget.innerText);
	}
</script>

上面的代码就可以实现这个需求了,但是,现在仔细想一想,问自己几个问题:

  • ulli 是不是嵌套的关系?
  • li 上绑定的事件是不是默认会冒泡到 ul 上?

上面两个问题的答案是肯定的,所以,你发现了上面代码的问题所在了吗——通过 forEach 循环为每个 li 元素都设置了事件监听,但其实这非常没有必要,反正最后 li 上绑定的事件都会默认冒泡到 ul 上,所以,我们直接在 ul 上设定一次事件绑定不就行了?!只要点击了 liul 上的事件监听就会被触发:

const ul = document.querySelector('ul');

ul.addEventListener('click', show);

function show(e) {
	console.log(e.target.innerText);
}

现在,只要点击 lishow 方法就会触发,通过 event.target 就能获取当前点击的元素是哪个。

这就是事件委托,它的意思是:不必给每个子元素都绑定一遍事件监听,只需要给它们的父元素绑定一次,然后通过事件冒泡来触发事件就可以了


事件目标

当我们通过点击或者是别的方式,来触发一个元素的事件监听时,我们往往需要获取到所触发的这个元素的 DOM节点对象 ——事件目标。一共有三个属性可以获取到事件目标对象,但它们之间稍微有些不同

event.target

上面实现事件委托依靠的就是 event.target ,它表示的是 事件触发的元素。

还是引用上面的例子:当我们点击 li 的时候,event.target 就是 li ,但如果我们点击的是 h1 ,那么 event.target 就是 h1 ,所以,在事件触发的时候,你点击的元素是谁,event.target 就是谁(换成别的任何事件类型同样适用)

event.currentTarget

它指的是:事件绑定的元素,而 event.target 则总是事件触发的元素。如何区分?在上面的例子中,事件 show 是绑定在 ul 上的,所以有如下推断:当我们点击 li 的时候

  • event.currentTarget 表示的是 ul
  • event.target 表示的是当前所点击的元素,也就是 li

我们改一下之前的代码,然后点击 li,再看看打印结果,检验我们的推断是否正确:

const ul = document.querySelector('ul');

ul.addEventListener('click', show);

function show(e) {
	console.log('target: ', e.target); // 事件触发的 dom 元素
  
	console.log('currentTarget: ', e.currentTarget); // 事件绑定的 dom 元素
}

最后,还有个地方需要注意一下,浏览器还实现了一个叫 event.srcElement 的属性,它的作用跟 event.target 是一样的,但是 event.srcElement 仅仅是为了支持 IE 浏览器而开发的属性,所以,你在开发的时候如果不需要适配 IE ,那么你使用 event.target 就好了。珍爱生命,远离 IE!

如果你没看懂,那么下面是参考资料,这样你就可以直接从源头处开始,自己研究: