标签属性 Tag Attributes
- 事件以 onXXX 的形式作为元素的一个属性 Properties 写在结构元素上;事件处理逻辑可以写在结构中也可以写在脚本中
- 可以携带参数
- 内联事件 - 只能满足当前元素的需求
- 只能处理冒泡阶段
- 适合简单逻辑
- 不符合结构 - 样式 - 逻辑分离的开发原则,较少使用
[] 完全耦合 - 全部写在结构中
<div onclick="alert('hi,there.')">点击我</div>
[] 部分耦合 - 结构逻辑分离
<div onclick="fn()">点击我</div>
function fn() {
alert('hi,there.')
}
[] 部分耦合 - 携带形参;参数类型可以是简单数据类型,也可以是复杂数据类型
<div onclick="fn_para({id:1,name:'glpla'})">点击我</div>
function fn_para(p) {
console.log(this);
console.log(p);
}
实例属性 Instance Properties
- 以实例属性 Properties 的形式分配事件 onXXX
- 先获取对象实例再分配事件 - 脚本中处理
- 兼容性好
- 只能满足当前元素;且元素只能绑定一个事件
- 事件函数可以接受参数;可以使用形参 e 获取事件对象;可以使用 this 获取执行事件函数的元素;也可以显式的指明参数
- 需要等页面加载完成 onload 后才能获取元素
- 不方便传递参数
[] onXXX 事件 - 默认形参 this 和事件对象 e
const el = document.querySelector('div')
el.onclick = function () {
console.log(this);
}
const el = document.querySelector('div')
el.onclick = function (e) {
console.log(e);
console.log(this);
}
如果使用箭头函数,则 this 指向为 window
事件对象为形参,可以使用任何合法标识符,通常使用 e 或 event
事件侦听 Event AddEventListene
- 为元素添加/注册事件监听器,允许注册多个
- 支持事件捕获、冒泡阶段处理,更容易管理事件
- 符合结构 - 样式 - 逻辑分离的开发原则
1. 事件注册 addEventListener
el.addEventListener(event-type, event-handle, capture)
- event-type - 不需要带 on,如click、mouseover
- event-handle - 当事件发生时调用的函数,也叫响应;可以使用形参 e 获取事件对象,访问事件的详细信息
- capture - 事件处理的时机,默认是冒泡 false;如果为 true,则是捕获;实际开发中,更关注冒泡
2. 事件移除 removeEventListener
el.removeEventListener(event-type, event-handle, capture)
- 性能考虑,有事件注册就应该有事件清除
- 正常起见,必须传入与注册中完全相同的函数引用;怎么注册就怎么清除
- 避免调试干扰:如果你在开发阶段频繁刷新或测试页面跳转逻辑,保留未解绑的监听器可能会导致行为异常,例如旧监听器被意外触发
- 防止某些特殊情况下的内存泄漏:在一些老旧浏览器或复杂 SPA 架构中,如果组件/模块没有正确销毁,监听器未解绑可能导致内存无法释放
- 编码规范 & 可维护性:主动解除绑定是一种良好的编程习惯,有助于代码可读性和资源管理意识
[] 绑定多个事件侦听器
//具名函数
wrap.addEventListener('click', fn)
//匿名函数;无法清除
wrap.addEventListener('click', (e) => {
console.log('wrap', e.target);
})
function fn(e) {
console.log('wrap fn', e.target);
}
//在另外一个元素的点击事件中,移除 wrap 的事件侦听器,只有 fn 可以被移除;另外一个事件继续生效
card.addEventListener('click', (e) => {
console.log('card', e.target);
wrap.removeEventListener('click', fn)
})
部分事件如:blur / focus / submit / change / reset / select / mouseleave / mouseenter 没有冒泡
事件处理函数尽量不要采用匿名函数的方式,否则无法清除
适用于函数表达式
使用开发者工具 → 事件侦听器,查看事件的添加和移除
使用 beforeunload 比 unload 更合适;unload 的兼容性和执行稳定性较差(部分浏览器限制 JS 执行)
- 不能直接使用,否则会被立即执行
- 需要使用匿名函数,在匿名函数里执行封装的带参函数
[] 封装表单域的获取焦点和失去焦点函数
function setBg(color) {
console.log('hi');
console.log(ta);
ta.style.background = color;
}
获取元素并侦听事件,发现函数被立即执行了其中的log,但是元素并没有按照设想改变颜色;继续交互同样不会执行
ta.addEventListener('focus', setBg('#f40'));
ta.addEventListener('blur', setBg('#ccc'));
将封装的带参函数,放在匿名函数中执行。问题解决
ta.addEventListener('focus', function () {
setBg('#f40')
});
ta.addEventListener('blur', function () {
setBg('#ccc')
});
也可以使用箭头函数,同时获取事件对象e
ta.addEventListener('focus', (e) => {
console.log(e);
setBg('#f40')
});
ta.addEventListener('blur', (e) => {
console.log(e);
setBg('#ccc')
});
- 事件产生时,由系统自动创建;包含事件的详细信息
- 在事件处理函数中作为形式参数,名字任意,通常使用 event、e、evt 等语义化名字
- 包含事件类型 bubbles [冒泡与否]、发生位置 [screen、page]、大小 [offset、client]、时间 timestamp、响应事件的目标 target
等信息;如果有多个元素或多层嵌套,则响应事件的目标元素也不同
- 可以利用事件对象阻止 默认事件
- 如果元素使用了 data- 自定义数据,应该在目标元素 target 的 dateset 去获取
事件目标 target
- 事件对象的一个属性,表示实际触发事件的元素
- target 始终指向实际触发事件的元素,而不是绑定事件监听器的元素,无论该元素是否绑定事件;即:与元素是否绑定事件监听器无关,只要元素存在于 DOM 中并且被点击,它就会成为 target 的值
- 事件监听器只是决定了哪个函数会被执行,但不影响 e.target 的值
当前事件目标 currentTarget
- 响应事件的当前目标;随着事件流的变化,事件当前目标也在变化
- 中间的元素只有绑定事件,才会有事件对象;如果没有绑定事件,依然会在事件流中,但是不会有响应
冒泡 Bubbling 和捕获 Capture
- 页面接受事件的顺序,即:事件发生时,在元素节点之间按照特定的顺序传播的过程
- 三个阶段:捕获阶段 capture phrase → 目标阶段 target phrase → 冒泡阶段 bubbling phrase
- 捕获阶段:从元素的根节点/祖先元素开始,向下传播,直到到达目标元素
- 目标阶段:事件到达目标元素
- 冒泡阶段:事件逐层向上传递回根元素
- 先捕获事件才能处理;事件捕获:网景提出;事件冒泡:微软提出|IE提出
- JavaScript 只能 执行冒泡或捕获中的一个阶段;默认是冒泡
- 使用 e.stopPropagation | e.cancelBubble 可以取消冒泡
- 更多信息,请访问 一个DOM元素绑定多个事件时,先执行冒泡还是捕获
stopPropagation 和 cancelBubble 都可以阻止浏览器默认的事件冒泡行为
cancelBubble 方法不符合W3C标准,只支持 IE 浏览
stopPropagation 符合 W3C 标准,适用于 FireFox 等浏览器,不支持 IE
[] 冒泡事件和捕获事件
. 三个嵌套盒子
<div class="box1">box1
<div class="box2">box2
<div class="box3">box3</div>
</div>
</div>
1. 冒泡:默认方式;单击里层的元素,事件会逐步冒泡到外层
. 单击 box1,输出 box1 clicked
. 单击 box2,从 box2 开始,向上冒泡到 box1,依次输出 box2 clicked、box1 clicked
. 单击 box3,从 box3 开始,逐步向上冒泡到 box2、box1,依次输出 box3 clicked、box2 clicked、box1 clicked
const box1 = document.querySelector('.box1')
const box2 = document.querySelector('.box2')
const box3 = document.querySelector('.box3')
box1.addEventListener('click', (e) => {
console.log('box1 clicked');
})
box2.addEventListener('click', (e) => {
console.log('box2 clicked');
})
box3.addEventListener('click', (e) => {
console.log('box3 clicked');
})
2. 捕获:开启捕获;单击里层的元素,事件首先由外层捕获并逐步传递到内层
. 单击 box1,输出 box1 clicked
. 单击 box2,首先由 box1 捕获并向内层传递,依次输出 box1 clicked、box2 clicked
. 单击 box3,首先由 box1 捕获并向内层传递,依次输出 box1 clicked、box2 clicked、box3 clicked
let box1 = document.querySelector('.box1')
let box2 = document.querySelector('.box2')
let box3 = document.querySelector('.box3')
box1.addEventListener('click', (e) => {
console.log('box1 clicked');
}, true)
box2.addEventListener('click', (e) => {
console.log('box2 clicked');
}, true)
box3.addEventListener('click', (e) => {
console.log('box3 clicked');
}, true)
. 借助事件对象e,取消事件传递;如果是冒泡,单击哪个元素,触发哪个元素的事件;如果是捕获,则始终是顶层元素
box1.addEventListener('click', (e) => {
e.stopPropagation();
console.log('box1 clicked');
})
box2.addEventListener('click', (e) => {
e.stopPropagation();
console.log('box2 clicked');
})
box3.addEventListener('click', (e) => {
e.stopPropagation();
console.log('box3 clicked');
})
[] target 和 currentTarget - 以冒泡为例
. 单击 outer:点的是 outer;向上没有父级或者说没有冒泡,响应事件的也是 outer,所以 target 和 currentTarget 都是 outer
. 单击inner:首先 inner 响应事件,所以 target 和 currentTarget 都是 inner;然后事件冒泡到 outer,outer 响应的是 inner 绑定的事件,所以
target 是 inner,currentTarget 是 outer
<div class="outer">
<div class="inner"></div>
</div>
outer.addEventListener('click', (e) => {
console.log('outer', e.target, e.currentTarget);
})
inner.addEventListener('click', (e) => {
console.log('inner', e.target, e.currentTarget);
})
如果只有一个元素,则 currentTarget 为 null;所以更多的是操作 target
- 单击关闭按钮关闭模态框
- 单击背景区域按钮关闭模态框
- 对象的类名、ID、标签名都可以作为判定的依据,但是必须能唯一确定
const mask = document.getElementById("mask")
const close = document.getElementById("close")
mask.addEventListener('click', (e) => {
if (e.target.id === 'mask') {
console.log('close');
}
})
close.addEventListener('click', (e) => {
if (e.target.id === 'close') {
console.log('close');
}
})
[] 共用事件 - 汉堡菜单的展开和折叠
为菜单类或容器类增加或减少状态类实现
移花接木
.nav {
left: -300px;
transition: 0.4s;
}
body.open .nav {
left: 0;
}
const navs = document.querySelectorALL('.nav');
const openBtn = document.getElementById('open');
const closeBtn = document.getElementById('close');
openBtn.addEventListener('click', () => {
document.body.classList.toggle('open')
})
closeBtn.addEventListener('click', () => {
openBtn.click()
})
navs.forEach(nav=>{
nav.addEventListener('click', (e)=>{
openBtn.click()
})
})
[] 单元素自定义数据 data- 的获取
- 事件对象e对应的target、currentTarget相同
- 注意函数 fn 的封装
<div class="tmp" data-id="1001">单元素自定义数据的获取</div>
let el = document.querySelector('.tmp');
//传统方式 - 元素属性
el.onclick = fn;
//侦听方式
el.addEventListener('click', fn)
function fn(e) {
console.log(this.getAttribute('data-id'));
// 推荐
console.log(e.target.dataset.id);
}
[] 父元素自定义数据 data- 的获取
- 多个元素存在,data- 数据在父级元素
- 事件目标通常是子元素,如果使用 dataset,就会导致自定义数据获取失败;只能使用 this 来获取属性
<div class="tmp" data-id="1001">
<div>child</div>
<div>child</div>
</div>
使用同样的 JavaScript,则第2个 log 是 undefined。因为事件目标是子元素,它没有对应的自定义数据
[ ] 破解方案
方案1. 判断目标是否是当前标签[要采用大写形式],如果不是,就查找其父元素;直到找到最外层的父级;也可以用根据当前目标是否有某个属性判断
function fn(e) {
console.log(this.getAttribute('data-id'));
let tar = e.target
while (tar.nodeName != 'DIV') {
tar = tar.parentNode
}
console.log(tar.dataset.id);
}
方案2. 取消父级所有后代的事件响应,只保留父元素的事件行为,所以事件目标一定就是父元素;简单粗暴
.tmp * {
pointer-events: none;
}
- 默认情况下,子元素的事件会冒泡到父元素
- 所以父元素可以响应任何一个子元素的事件;利用这个特性可以让父元素作为各个子元素的 Event
Delegation
- 适合子元素多,且动态变化的情况
- 单击 outer,target 是 outer;没有冒泡,currentTarget 也是 outer
- 单击 <p>,target 是 <p>;冒泡到 outer,currentTarget 是 outer
- 同理,单击 <span>,target 是 <span>;冒泡到 outer,currentTarget 是 outer
<div class="outer" @click="doOut">
<p class="inner">1111</p>
<span class="inner">2222</span>
</div>
通过判断 target 来确定单击了哪个子元素;可以根据子元素的标签名(大写)或自定义数据 data-判断
const doOut = (e) => {
console.log(e.target.tagName);
console.log(e.target.dataset.id);
console.log(e.target.innerHTML);
}
[] 模态框 - 代理
- 仅 mask 绑定事件;如果事件对象 target 的 id 是 mask 或 close,就关闭模态框
const mask = document.getElementById("mask")
mask.addEventListener('click', (e) => {
if (e.target.id === 'mask' || e.target.id === 'close') {
console.log('close');
}
})
[] 单击标签页,显示对应的内容
<ul>
<li>Lorem.</li>
<li>Magni.</li>
<li>Nihil.</li>
<li>Id?</li>
<li>Ipsa!</li>
<li>Earum?</li>
<li>Laboriosam.</li>
<li>Voluptatum?</li>
<li>Veniam.</li>
<li>Aliquid.</li>
</ul>
1. 默认方式 - 遍历所有子元素并绑定事件
let ul = document.querySelector('ul');
let lis = ul.querySelectorAll('li');
lis.forEach(li => {
li.addEventListener('click', () => {
console.log(li.innerHTML);
})
})
2. 事件委托方式 - 给父级元素指定事件,只有 target 是 LI 才响应,即:只有点到了 LI 才响应
let ul = document.querySelector('ul');
ul.addEventListener('click', (e) => {
console.log(e.target.tagName);
if(e.target.tagName==='LI'){
//do
}
})