认识事件h2
Web页面需要经常和用户之间进行交互,而交互得过程中我们可能想要捕获这个交互得过程
例如,用户点击了某个页面、用户在输入狂里输入了某个文本,用户鼠标经过了某个位置
浏览器需要搭建一条js代码和事件之间的桥梁,当某个事件发生时,让js可以相应(执行某个函数),所以我们需要针对事件编写处理程序(handler)
那么事件如何监听呢?
- 方式一:直接在 HTML 标签上通过
on+事件名属性写 JS 代码(或调用函数),是最早期的写法,几乎不推荐使用。
<!-- 写法1:直接写代码 --><button onclick="alert('你点击了按钮')">点击我</button>
<!-- 写法2:调用外部函数 --><button onclick="handleClick()">点击调用函数</button>
<script> function handleClick() { console.log('按钮被点击了') }</script>- 方式二:DOM 属性监听(
onXXX方式)
通过 JS 获取 DOM 元素,给元素的 on+事件名 属性赋值为处理函数,是比内联更优的写法,但仍有局限性。
<button id="btn">点击我</button>
<script> // 1. 获取DOM元素 const btn = document.getElementById('btn')
// 2. 绑定事件处理程序 btn.onclick = function () { console.log('按钮被点击(DOM属性方式)') console.log('当前元素:', this) // this 指向触发事件的元素(btn) }
// 3. 覆盖事件(注意:多次赋值会覆盖) btn.onclick = function () { console.log('新的点击处理程序(覆盖了之前的)') }
// 4. 移除事件:赋值为 null // btn.onclick = null;</script>- 方式三:通过EventTarget中得adddEventListener来监听
EventTarget 是 DOM 元素的基类,所有元素都继承了 addEventListener 方法,用于灵活绑定事件处理程序,是现代 Web 开发的标准写法。
元素.addEventListener(事件名, 处理函数, [配置项]);
- 事件名:比如click、mouseover、mousemove
- 处理函数:事件触发时执行的函数(箭头函数的 this 指向全局,普通函数指向元素)。
- 配置项:可选,布尔值(
true= 捕获阶段触发,false= 冒泡阶段触发,默认 false)或对象(如{ once: true }仅触发一次)。
事件流h2
Event flow指的是浏览器处理DOM事件时,事件在页面中传播的完整路径和阶段。
当用户触发一个事件(比如点击按钮),事件并非只作用在目标元素上,而是会从最外层的根节点(如document)开始,经过一系列传播过程,最终到达目标元素,再反向回到根节点 —— 这个 “传播轨迹” 就是事件流。
事件流的本质是:DOM 事件不是孤立触发的,而是沿着 DOM 树在不同元素间传播的过程。
<head> <style> .box { width: 200px; height: 200px; background-color: orange; display: flex; justify-content: center; align-items: center; }
.box span { width: 100px; height: 100px; background-color: red; } </style></head>
<body> <div class="box"> <span></span> </div>
<script> var spanELem = document.querySelector('span') var divELem = document.querySelector('div') var bodyElem = document.querySelector('body')
spanELem.onclick = function () { console.log('spanELem click') }
divELem.onclick = function () { console.log('divELem click') }
bodyElem.onclick = function () { console.log('bodyElem click') } </script></body>当点击最内层红色块
点击div部分橘色块
点击最外层无色块
根据上述实验,我们可以发现默认情况下,事件是从最内层依次向外传递得顺序,这个过程就称之为事件冒泡(Event Bubble)
对于事件冒泡,不管是普通的onclick,还是addEventListener,默认都是冒泡
但事实上,还有一种监听事件流得方式是从外往内层,这种称之为事件捕获(Event Capture)
想要设置为事件捕获,需要在addEventListener中添加一个参数capture,值为true,上文也提到过了
<head> <style> .box { width: 200px; height: 200px; background-color: orange; display: flex; justify-content: center; align-items: center; }
.box span { width: 100px; height: 100px; background-color: red; } </style></head>
<body> <div class="box"> <span></span> </div>
<script> var spanELem = document.querySelector('span') var divELem = document.querySelector('div') var bodyElem = document.querySelector('body')
spanELem.addEventListener( 'click', function () { console.log('spanELem click') }, true )
divELem.addEventListener( 'click', function () { console.log('divELem click') }, true )
bodyElem.addEventListener( 'click', function () { console.log('bodyElem click') }, true ) </script></body>传进去一个event参数,通过event.eventPhase获取阶段
| 返回值 | 对应阶段 | 说明 |
|---|---|---|
| 1 | CAPTURING_PHASE | 事件从外到内传播的阶段 |
| 2 | AT_TARGET | 事件命中实际触发的元素 |
| 3 | BUBBLING_PHASE | 事件从内到外回溯的阶段 |
下面的例子我们给每个元素(body/div/span)都绑了两次点击事件:
第一次:addEventListener(..., true) → 捕获阶段触发(对应 eventPhase=1)
第二次:addEventListener(..., false)(默认)→ 冒泡阶段触发(对应 eventPhase=3)
<head> <style> .box { width: 200px; height: 200px; background-color: orange; display: flex; justify-content: center; align-items: center; }
.box span { width: 100px; height: 100px; background-color: red; } </style></head>
<body> <div class="box"> <span></span> </div>
<script> var spanELem = document.querySelector('span') var divELem = document.querySelector('div') var bodyElem = document.querySelector('body')
spanELem.addEventListener( 'click', function (event) { console.log('spanELem click') console.log(event.eventPhase, event.target, event.currentTarget) }, true )
divELem.addEventListener( 'click', function (event) { console.log('divELem click') console.log(event.eventPhase, event.target, event.currentTarget) }, true )
bodyElem.addEventListener( 'click', function (event) { console.log('bodyElem click') console.log(event.eventPhase, event.target, event.currentTarget) }, true )
spanELem.addEventListener('click', function (event) { console.log('spanELem click') console.log(event.eventPhase, event.target, event.currentTarget) })
divELem.addEventListener('click', function (event) { console.log('divELem click') console.log(event.eventPhase, event.target, event.currentTarget) })
bodyElem.addEventListener('click', function (event) { console.log('bodyElem click') console.log(event.eventPhase, event.target, event.currentTarget) }) </script></body>
从结果来看,event.currentTarget是当前触发得元素,而event.target是实际触动的元素。我们实际点击的是span标签,所以event.target一直都是span标签,而event.currentTarget会一直改变。
从上述例子也可以看出,事件流的执行顺序是浏览器内置的规则,捕获阶段天然在冒泡阶段之前触发。
| 控制台输出 | eventPhase值 | 对应阶段 & 逻辑 |
|---|---|---|
| bodyElem click 1 | 1 | 捕获阶段:事件从外到内,先触发body的捕获事件 |
| divELem click 1 | 1 | 捕获阶段:继续向内,触发div的捕获事件 |
| spanELem click 2 | 2 | 目标阶段:事件命中span,触发span的捕获事件(目标阶段eventPhase=2) |
| spanELem click 2 | 2 | 目标阶段:触发span的冒泡事件(目标阶段不分捕获 / 冒泡,eventPhase都是2) |
| divELem click 3 | 3 | 冒泡阶段:事件从内到外,触发div的冒泡事件 |
| bodyElem click 3 | 3 | 冒泡阶段:继续向外,触发body的冒泡事件 |
两个方法:
stopPropagation():阻止事件冒泡或者捕获preventDefault():阻止默认行为
preventDefault() 只是阻止默认行为,不会阻止事件的捕获/冒泡阶段(如果想阻止事件传播,要用 event.stopPropagation())。
<body> <div class="user-record"></div> <form class="form-of-user"> <textarea id="user-prompt"></textarea> <input type="text" id="user-name" /> <button type="submit" id="user-submit">submit</button> </form>
<script> var userRecord = document.querySelector('.user-record')
var submitBtn = document.querySelector('#user-submit') submitBtn.onclick = function (e) { // 阻止表单默认提交(页面不刷新) e.preventDefault() var userNameValue = document.querySelector('#user-name').value var userRecordValue = document.querySelector('#user-prompt').value
var newRecord = document.createElement('div') newRecord.textContent = userNameValue + ': ' + userRecordValue
if (userNameValue && userRecordValue) { userRecord.prepend(newRecord) } } </script></body>上述示例中,我们给submit按钮绑定了click事件,点击按钮时,会触发表单的默认提交行为,即页面刷新。为了阻止默认行为,我们给click事件绑定了e.preventDefault()方法。
EventTargeth2
我们已经知道所有的节点、元素都继承自 EventTarget
事实上,window 也继承自 EventTarget
那么这个 EventTarget 是什么呢?
是一个 DOM 接口,主要用于添加、删除、派发 Event 事件
常见的方法:
- addEventListener:注册某个事件类型以及事件处理函数
- removeEventListener:删除某个事件类型以及事件处理函数
- dispatchEvent:派发某个事件类型到 EventTarget 上
<body> <button class="btn-1">点老子</button> <script> // 现在有个需求 一开始需要监听这个按钮的点击,过5s后,将这个事件监听移除掉 var btnElem = document.querySelector('.btn-1') btnElem.addEventListener('click', function () { console.log('监听到按钮的点击') })
setTimeout(function () { btnElem.removeEventListener('click', function () { console.log('监听到按钮的点击') }) }, 5000) </script></body>事实上,这个移除不了,会一直监听,为什么?
因为removeEventListener的第二个参数需要和 addEventListener的第二个参数是同一个函数,下面这一版才是正解
<body> <button class="btn-1">点老子</button> <script> // 现在有个需求 一开始需要监听这个按钮的点击,过5s后,将这个事件监听移除掉 var btnElem = document.querySelector('.btn-1')
var func = function () { console.log('监听到按钮的点击') }
btnElem.addEventListener('click', func)
setTimeout(function () { btnElem.removeEventListener('click', func) }, 5000) </script></body>派发事件h2
dispatchEvent 方法用于派发事件,这个方法接收一个参数,这个参数是一个 Event 对象,这个对象可以自己创建
window.addEventListener('Arthur', function () { console.log('Hello...yes, speaking...')})
var event = new Event('Arthur')
setTimeout(function () { window.dispatchEvent(event)}, 5000)事件委托h2
假设有一个页面,页面上有多个按钮,点击按钮后,会触发一个事件,但是这个事件需要绑定在多个按钮上,那么有没有一种方法,可以只绑定一个事件,然后通过事件委托,来处理多个按钮的事件呢?
其实就是使用上文我们提到的事件冒泡,子类节点点击,冒泡到父类节点,父元素可以监听到子元素的点击,并且可以通过event.target获取到当前监听的元素
下面有个案例:一个ul中存放多个li,点击某一个li,会变成红色
原始的思路是,监听每一个li的点击,并且做出回应
<head> <style> .active { color: red; } </style></head>
<body> <ul> <li>3</li> <li>4</li> <li>5</li> <li>6</li> <li>7</li> </ul>
<script> // 原始做法 var liElems = document.querySelectorAll('li') for (var liElem of liElems) { ;(function (item) { item.addEventListener('click', function () { item.classList.add('active') }) })(liElem) } // 或者使用 let liElem of liElems 块级作用域 </script></body>这种思路还有一些别的实现方式,比如使用监听currentTarget,或者用this
<head> <style> .active { color: red; } </style></head>
<body> <ul> <li>3</li> <li>4</li> <li>5</li> <li>6</li> <li>7</li> </ul>
<script> // 原始做法 var liElems = document.querySelectorAll('li') for (var liElem of liElems) { liElem.addEventListener('click', function (event) { event.currentTarget.classList.add('active') // this.classList.add('active') // 默认绑定为 event.currentTarget }) } </script></body>但是不管是什么实现方式,只要用了循环,就意味着为多个元素都绑定了一个函数,每个函数操作都是一样的,很冗余
事件委托模式:在ul中监听,并且通过event.target判断点击的是哪一个li,然后做出回应
<head> <style> .active { color: red; } </style></head>
<body> <ul> <li>3</li> <li>4</li> <li>5</li> <li>6</li> <li>7</li> </ul>
<script> // 使用事件委托,交给父元素监听并处理 var ulElem = document.querySelector('ul') ulElem.addEventListener('click', function (event) { var target = event.target target.classList.add('active') }) </script></body>现在有个新的需求,点击某一个,这一个变红,但是其他变红的恢复原状
<head> <style> .active { color: red; } </style></head>
<body> <ul> <li>3</li> <li>4</li> <li>5</li> <li>6</li> <li>7</li> </ul>
<script> var ulElem = document.querySelector('ul') ulElem.addEventListener('click', function (event) { // 新需求,点击某一个,这一个变红,但是其他变红的恢复原状
// 方法一:遍历所有子元素,判断是否包含active,有则移除 // for(var i = 0; i < ulElem.children.length; i++){ // if(ulElem.children[i].classList.contains("active")){ // ulElem.children[i].classList.remove("active") // } // }
// 方法二:使用querySelectorAll直接找到,然后移除active // var activeElem = document.querySelectorAll('.active') // activeElem.forEach(function (item) { // item.classList.remove('active') // })
// 方法三:这里因为我们已知每次最多有一个变红,所以可以直接querySelector(".active") activeElem = document.querySelector('.active') activeElem && activeElem.classList.remove('active')
var target = event.target target.classList.add('active') }) </script></body>但是现在还有一个问题,那就如果我们点击ul,那么所有的li都会变红,这显然是不符合要求的
最简单的思考,if(event.target.tagName!=="UL")我们套一层逻辑,如果点击的元素不是ul,那么就返回
var ulElem = document.querySelector('ul')ulElem.addEventListener('click', function (event) { // 希望点击ul不要触发 if (event.target.tagName !== 'UL') { activeElem = document.querySelector('.active') activeElem && activeElem.classList.remove('active')
var target = event.target target.classList.add('active') }})但是这样太依赖于tagName,如果以后tagName被改变,那么代码就会出错
var ulElem = document.querySelector('ul')var activeElem = nullulElem.addEventListener('click', function (event) { // 如果存在activeElem并且不是ulElem,则移除active样式 if (activeElem && event.target !== ulElem) { activeElem.classList.remove('active') }
// 如果不是ulElem,则添加active样式 if (event.target !== ulElem) { event.target.classList.add('active') }
// 给activeElem重新赋值,保存当前点击的元素 activeElem = event.target})另一个案例,现在有三个按钮,我希望我点击哪个,就console.log哪个的值
<body> <div class="box"> <button>移除</button> <button>新增</button> <button>搜索</button> </div> <script> var boxElem = document.querySelector('.box') boxElem.addEventListener('click', function (event) { if (event.target != boxElem) { if (event.target.textContent === '移除') { console.log('移除') } else if (event.target.textContent === '新增') { console.log('新增') } else if (event.target.textContent === '搜索') { console.log('搜索') } } }) </script></body>或者我们可以为三个button加上data-*,然后根据*来输出
<body> <div class="box"> <button data-action="remove">移除</button> <button data-action="add">新增</button> <button data-action="search">搜索</button> </div> <script> var boxElem = document.querySelector('.box')
boxElem.addEventListener('click', function (event) { var btnElem = event.target var action = btnElem.dataset.action switch (action) { case 'remove': console.log('移除') break case 'add': console.log('新增') break case 'search': console.log('搜索') break } }) </script></body>
Comments