JS事件流
事件流
@jarringslee
文章目录
- 事件流
- 事件捕获
- 事件冒泡
- 阻止冒泡
- 事件解绑
- 两种注册事件方法
- 事件委托
- 用事件对象阻止默认行为
- 其他类型事件
- 获取元素的尺寸与位置
- 日期对象
- 实例化 new
- 日期对象方法
- 时间戳
事件流是时间完整执行过程的流动路径
大到小:事件捕获;小到大:事件冒泡
事件捕获
目标: 简单了解事件捕获执行过程
-
概念: 从DOM的根元素开始去执行对应的事件(从外到里)。
-
事件捕获需要写对应代码才能看到效果。
-
代码:
DOM.addEventListener(事件类型,事件处理函数,是否使用捕获机制)
-
说明:
addEventListener
的第三个参数传入true
代表在捕获阶段触发(很少使用)。- 若传入
false
代表在冒泡阶段触发,默认值为false
。 - 如果使用 L0 事件监听(如
onclick
),则只有冒泡阶段,没有捕获阶段
事件冒泡
目标: 能够说出事件冒泡的执行过程
概念:
当一个元素的事件被触发时,同样的事件将会在该元素的所有祖先元素中依次被触发。这一过程被称为事件冒泡。
简单理解:当一个元素触发事件后,会依次向上调用所有父级元素的同名事件。
- 事件冒泡是默认存在的。
- L2 事件监听(
addEventListener
)的第三个参数是false
,或者默认不传时,都是冒泡阶段触发。
代码示例:
const father = document.querySelector('.father')
const son = document.querySelector('.son')
document.addEventListener('click', function () { alert('我是爷爷') })
father.addEventListener('click', function () { alert('我是爸爸') })
son.addEventListener('click', function () { alert('我是儿子') })
执行过程说明:
- 点击
son
元素时,会依次触发:son
的点击事件 → 弹出 “我是儿子”father
的点击事件 → 弹出 “我是爸爸”document
的点击事件 → 弹出 “我是爷爷”
- 事件从触发元素向外(父级)逐层传播,形成冒泡。
鼠标经过事件:
- mouseover和mouseout会有冒泡效果
- mouseenter和mouseleave没有冒泡效果(推荐)
阻止冒泡
目标: 能够写出阻止冒泡的代码
问题: 由于事件冒泡是默认存在的,子元素的事件可能会意外触发父元素的相同事件,导致不必要的影响。
需求: 如果希望事件仅在当前元素内触发,不向上冒泡影响父级元素,就需要阻止事件冒泡。
前提: 阻止事件冒泡需要获取事件对象(Event Object)。
语法:
事件对象.stopPropagation()
代码示例:
const father = document.querySelector('.father');
const son = document.querySelector('.son');father.addEventListener('click', function() {alert('我是爸爸');
});son.addEventListener('click', function(e) {e.stopPropagation(); // 阻止事件冒泡alert('我是儿子');
});
执行效果:
- 点击
son
时,仅触发son
的事件(弹出 “我是儿子”),不会触发father
的事件。 - 如果不加
e.stopPropagation()
,点击son
会依次触发son
→father
的事件。
注意事项:
stopPropagation()
不仅阻止冒泡阶段,在捕获阶段也同样有效。- 适用于需要精确控制事件传播的场景,如模态框、下拉菜单等。
事件解绑
L0 解绑方式
// 绑定事件
btn.onclick = function() { ... }// 解绑方式:直接赋值为 null
btn.onclick = null
特点:直接覆盖原始事件处理函数
L2 解绑方式 (addEventListener)
// 绑定事件
function handleClick() { ... }
btn.addEventListener('click', handleClick)// 解绑方式:使用相同参数
btn.removeEventListener('click', handleClick)
关键要求:
- 事件类型相同
- 必须是同一个函数引用
- 捕获阶段需与绑定时一致
匿名函数和箭头函数无法被解绑
// 匿名函数无法解绑
btn.addEventListener('click', function() { ... })// 箭头函数无法解绑(匿名特性)
btn.addEventListener('click', () => { ... })
原因:
解绑需要函数引用,匿名函数无法被二次引用
- 需要解绑的事件 → 使用具名函数 + L2方式
- 一次性事件 → 在函数内使用
removeEventListener
自解绑- 不需要解绑 → 可用匿名函数
两种注册事件方法
-
传统on注册(L0)
- 同一个对象,后面注册的事件会覆盖前面注册的事件(同一个事件)
- 直接使用null覆盖就可以实现事件的解绑
- 都是冒泡阶段执行的
-
事件监听注册(L2)
- 语法:addEventListener(事件类型,事件处理函数,是否使用捕获)
- 后面注册的事件不会覆盖前面注册的事件(同一个事件)
- 可以通过第三个参数控制冒泡或捕获阶段执行
- 必须使用removeEventListener(事件类型,事件处理函数,捕获或冒泡阶段)
- 匿名函数无法被解绑
特性 | 传统 on 注册(L0) | 事件监听注册(L2) |
---|---|---|
语法 | element.on事件 = 处理函数 | addEventListener(事件类型, 处理函数, 是否捕获) |
重复绑定 | 同事件后绑定的会覆盖前绑定的 | 同事件可绑定多个,按注册顺序依次执行 |
解绑方式 | element.on事件 = null | removeEventListener(事件类型, 处理函数, 捕获阶段) |
事件阶段 | 仅冒泡阶段执行 | 可通过第三个参数控制捕获(true )或冒泡(false )阶段 |
匿名函数处理 | 可直接覆盖解绑 | 匿名函数无法解绑(需使用具名函数引用) |
- 覆盖性:L0 会覆盖同名事件,L2 不会。
- 灵活性:L2 支持捕获/冒泡阶段控制,L0 仅冒泡。
- 解绑要求:L2 需严格匹配参数(尤其是函数引用),L0 直接赋
null
即可。 - 需要精细控制事件阶段或多函数绑定 → 优先用 L2
- 简单场景或快速解绑需求 → 可用 L0
事件委托
事件冒泡派上用场。
我们给多个元素注册事件(比如一堆小li),原来用的是for循环,现在可以用事件委托一步到位。
事件委托是利用事件流的特征解决一些开发需求的技巧。
- 优点 减少注册次数,提高程序性能
- 原理 利用事件冒泡的特点:
- 给父元素注册一个事件,当我们触发子元素的时候,会冒泡到父元素身上,从而触发父元素的事件。
<ul><li>111</li><li>222</li><li>333</li><li>444</li><li>555</li><p>泥嚎</p></ul><script>const ul = document.querySelector('ul')ul.addEventListener('click', function (e) {e.target.style.color = 'red'})</script>
为什么不用:
ul.addEventListener('click', function (e) {this.style.color = 'red' })
这样的话,点击任意一个子元素会让所有元素都变红。
- 这里的 this 是 ul,因为事件绑定在 ul 上。
- 所以这行代码是让整个 ul 变红。由于 li 是 ul 的子元素,它们继承了颜色,就都变红了。
只选取目标子元素,不影响其他子元素:
ul.addEventListener('click', function (e) {if (e.target.tagName === 'LI') {e.target.style.color = 'red'}
})
用事件对象阻止默认行为
preventDefault()
<body><form action="https://pvp.qq.com"><button>进入</button></form><script>const form = document.querySelector('form')form.addEventListener('submit', function (e) {e.preventDefault()//这个函数阻止了按下按钮提交并跳转网站的行为。})</script>
</body>
其他类型事件
-
页面加载事件
让外部资源(CSS、JS文件等)全部加载完毕之后再触发事件。
- 事件名:load
- 执行对象:window
如果js代码放在了body前面,则有些变量会处于未声明的状态:
<head> ......<script>const btn = document.querySelector('button')btn.addEventListener('click', function () {btn.style.backgroundColor = 'red'})</script> </head> <body><button>点我</button> </body>
这里无法使按钮变红,需要添加加载事件:
window.addEventListener('load', function () {const btn = document.querySelector('button')btn.addEventListener('click', function () {btn.style.backgroundColor = 'red'}) })
也可以作用于其他元素:
img.addEventListener('load', function () {//等待图片加载完毕,再执行该代码 })
只加载dom节点的事件: 无需等待其他样式、图表等完全加载,效率更高。
- 事件名:
DOMContentLoaded
- 添加对象:
document
document.addEventListener('DOMContentLoaded', function () {...... })
-
元素滚动事件
鼠标滚轮滚动后触发事件。
事件名:
scroll
执行对象:window、document等。
window.addEventListener('scroll', function () {console.log('滚。')//滚动一像素执行一次})
-
滚动距离属性(内容被卷去的尺寸大小)
- scrollTop scrollLeft
这两个值可以读写。
window.addEventListener('scroll', function () {console.log(document.documentElement.scrollTop)//获取HTML元素的写法 })
简单应用:
window.addEventListener('load', function () {const div = document.querySelector('div')window.addEventListener('scroll', function () {const n = document.documentElement.scrollTopconsole.log(n)if (n >= 300) {div.style.backgroundColor = 'pink'}})})
直接赋值:
window.addEventListener('load', function () {document.documentElement.scrollTop = '1000'})
点击按钮返回顶部:
const top1 = document.querySelector('#backTop')top1.addEventListener('click', function () {document.documentElement.scrollTop = 0})
或者也可以
//把document.documentElement.scrollTop = 替换成 window.scrollTo(0, 0)
-
-
页面尺寸事件
-
会在改变窗口尺寸时触发的事件
事件名:
resize
window.addEventListener('resize', function () {console.log('1') })
每次放大或是缩小会输出一次1。
-
-
resize用于检测屏幕宽度
获取元素可见部分的高:不包含边框、margin和滚动条等
元素名(可用于HTML元素):
clientWidth
clientHeight
//检测浏览器窗口宽度 window.addEventListener('resize', function () {let w = document.documentElement.clientWidthconsole.log(w)}) //检查某一元素高度 const div = document.querySelecotor('div') console.log(div.clientHeight)
获取元素的尺寸与位置
-
获取尺寸:宽和高
获取元素自身的宽和高的可视数值,包括自身设置的宽高、padding、border,如果盒子是隐藏的,那么获取的结果将会是0。
属性名:
offseWidth
offseHeight
-
获取位置:
获取元素自己定位父级元素的左、上的距离(会算上元素的外边距等),注意这个值只读。
- 如果该元素没有父元素或者父元素没有设置定位属性,那么该元素的值是相对于浏览器窗口的位置;
- 如果该元素有父级元素并且父级元素有定位(父元素设置相对定位),那么该元素的值是自己相对于父元素边界的值。
属性名:
offseLeft
offseTop
属性 | 作用 | 说明 |
---|---|---|
scrollLeft scrollTop | 被卷去的头部和左侧 | 配合页面滚动来用,只有这个是可读可写的;不包含 border、margin;滚动条用于 JS。 |
clientWidth clientHeight | 获得元素宽度和高度 | 获取元素大小,只读属性;包含 padding,不包含 border、margin。 |
offsetWidth offsetHeight | 获得元素宽度和高度 | 包含 border、padding、滚动条等,只读。 |
offsetLeft offsetTop | 获取元素位置 | 获取元素相对于其定位父级的左、上距离,只读属性。 |
“导航栏在滑动到一定高度时显示/隐藏”小案例
const ele = document.querySelector('.xtx-elevator')const entryh = document.querySelector('.xtx_entry')
//给浏览器窗口添加滚动事件window.addEventListener('scroll', function () {//检测窗口滚动量let n = document.documentElement.scrollTopconsole.log(n)//要是滚过某一元素距离浏览器顶部的量就隐藏ele.style.opacity = n >= entryh.offsetTop ? 1 : 0})
“窗口滑到某一模块出现顶部导航栏”小案例
//主页面中某一模块const sk = document.querySelector('.sk')//顶部导航栏(原来被top:-80px隐藏)const header = document.querySelector('.header')window.addEventListener('scroll', function () {const n = document.documentElement.scrollTopif (n >= sk.offsetTop) {header.style.top = 0} else {header.style.top = '-80px'}//if语句等价于header.style.top = n >= sk.offsetTop ? 0 :'-80px'})
“电梯导航栏跳转”小案例
const list = document.querySelector('.xtx-elevator-list')list.addEventListener('click', function (e) {//事件委托//选中list【拥有自定义属性名的】a标签(排除掉返回顶部的按钮)if (e.target.tagName === 'A' && e.target.dataset.name) {const old = document.querySelector('.xtx-elevator-list .active')//如果原来有元素有高亮效果,解除原有高亮效果 if (old) old.classList.remove('active') //当前事件对象添加高亮效果e.target.classList.add('active')//点击跳转模块:元素的offsetTop值直接复制给浏览器当前窗口滚动值document.documentElement.scrollTopdocument.documentElement.scrollTop = document.querySelector(`.xtx_goods_${e.target.dataset.name}`).offsetTop// 这里利用了模版字符串} }
“滑到该元素时对应按钮自动高亮”案例
//滑动到位置时自动高亮window.addEventListener('scroll', function () {//依旧先移除原有的高亮const old = document.querySelector('.xtx-elevator-list .active')if (old) old.classList.remove('active')//获取大模块的高const news = document.querySelector('.xtx_goods_new')const popular = document.querySelector('.xtx_goods_popular')const brand = document.querySelector('.xtx_goods_brand')const topic = document.querySelector('.xtx_goods_topic')const n = document.documentElement.scrollTop//手动添加条件,进入该模块范围那么对应的按钮就高亮if (n >= news.offsetTop && n < popular.offsetTop) {document.querySelector('[data-name = new]').classList.add('active')} else if (n >= popular.offsetTop && n < brand.offsetTop) {document.querySelector('[data-name = popular]').classList.add('active')} else if (n >= brand.offsetTop && n < topic.offsetTop) {document.querySelector('[data-name = brand]').classList.add('active')} else if (n >= topic.offsetTop) {document.querySelector('[data-name = topic]').classList.add('active')}
日期对象
实例化 new
new关键字能将一个对象实例化,日期对象也会用new来实例化
创建并获取当前时间:
-
获得当前时间
const date = new Date() //输出结果(当前精确时间): //Wed Jul 23 2025 10:25:45 GMT+0800 (中国标准时间)
-
手动获取事件
在括号中填入日期和时间,时间可以不填
const date1 = new Date('2024-1-5 10:00:00') //输出结果: //Fri Jan 05 2024 10:00:00 GMT+0800 (中国标准时间)
日期对象方法
日期对象返回的数据无法直接使用,需要转换为实际开发中常用的格式
方法 | 作用 | 说明 |
---|---|---|
getFullYear() | 获得年份 | 返回四位年份,如 2024 |
getMonth() | 获得月份 | 0–11(0 表示 1 月,11 表示 12 月) |
getDate() | 获取月份中的每一天 | 1–31(随月份变化) |
getDay() | 获取星期 | 0–6(0 表示星期日) |
getHours() | 获取小时 | 0–23 |
getMinutes() | 获取分钟 | 0–59 |
getSeconds() | 获取秒 | 0–59 |
输出月份和星期时需要+1
console.log(date.getFullYear())console.log(date.getMonth() + 1)
简单时间输出:
<body><p id="timeDisplay"></p><script>function padZero(n) {return n < 10 ? '0' + n : n}function showTime() {const now = new Date()const year = now.getFullYear()const month = padZero(now.getMonth() + 1) // 月份从0开始const day = padZero(now.getDate())const hour = padZero(now.getHours())const minute = padZero(now.getMinutes())const second = padZero(now.getSeconds())// 控制台输出console.log(`${year}-${month}-${day} ${hour}:${minute}`)// 页面输出document.getElementById('timeDisplay').textContent =`今天是${year}年${month}月${day}日 ${hour}时${minute}分${second}秒`}showTime()setInterval(showTime, 1000) // 初始显示</script>
</body>
页面显示当前的时间 :1.在工作台中显示YYYY-MM-DD HH:mm
;2.在浏览器界面显示“今天是xxxx年xx月xx日 xx时xx分xx秒”
- 调用用日期对象方法进行转换
- 数字要补0
<head><!-- ...... --><style>#timeBox {width: 300px;height: 40px;border: 1px solid #000;text-align: center;line-height: 40px;}</style>
</head><body><div id="timeBox"></div><script>function padZero(n) {return n < 10 ? '0' + n : n}function getTimeText() {const now = new Date()const year = now.getFullYear()const month = padZero(now.getMonth() + 1)const day = padZero(now.getDate())const hour = padZero(now.getHours())const minute = padZero(now.getMinutes())const second = padZero(now.getSeconds())// 控制台输出console.log(`${year}-${month}-${day} ${hour}:${minute}`)// 返回字符串供 innerHTML 使用return `今天是${year}年${month}月${day}日 ${hour}时${minute}分${second}秒`}const div = document.getElementById('timeBox')// 初始显示一次(回调函数最开始是不显示的,先提前放一次避免刷新后出现一秒的空白)div.innerHTML = getTimeText()// 每秒更新时间setInterval(function () {div.innerHTML = getTimeText()}, 1000)</script>
</body>
使用toLocaleString()
便捷输出时间
<head><meta charset="UTF-8"><title>toLocaleString 示例</title><style>#timeBox {width: 300px;height: 40px;border: 1px solid #000;text-align: center;line-height: 40px;}</style>
</head>
<body><div id="timeBox"></div><script>const div = document.getElementById('timeBox')function updateTime() {const now = new Date()div.innerHTML = '现在时间:' + now.toLocaleString()}updateTime() // 初始显示setInterval(updateTime, 1000) // 每秒更新</script>
</body>
toLocaleString()
根据浏览器语言显示格式;- 在英文系统中,它可能显示为
7/17/2025, 10:08:23 AM
; - 如果希望固定为中文格式,可以用:
now.toLocaleString('zh-CN')
时间戳
- 使用场景:
如果计算倒计时效果,前面方法无法直接计算,需要借助于时间戳完成 - 什么是时间戳: 是指1970年01月01日00时00分00秒起至现在的毫秒数,它是一种特殊的计量时间的方式
- 算法:
- 将来的时间戳 - 现在的时间戳 = 剩余时间毫秒数
- 剩余时间毫秒数 转换为 剩余时间的年月日时分秒 就是倒计时时间
- 比如:
将来时间戳 2000ms - 现在时间戳 1000ms = 1000ms
1000ms 转换为就是 0小时0分1秒
注:ECMAScript 中时间戳是以毫秒计的。
// 1. 实例化const date = new Date() // 2. 获取时间戳console.log(date.getTime())// 还有一种获取时间戳的方法console.log(+new Date())// 还有一种获取时间戳的方法console.log(Date.now())
getTime()
(推荐) +new Date() 无需士力架
Date.now() 无需实例化,但只能得到当前时间戳,上面两种方法逗你呢和返回指定时间的时间戳
代码实现
// 获取当前时间戳(毫秒)
const now = Date.now()// 计算倒计时示例(假设目标时间为2023-12-31)
const targetDate = new Date('2023-12-31')
const targetTime = targetDate.getTime() // 获取目标时间戳// 计算剩余时间(毫秒)
const remainingTime = targetTime - now// 将毫秒转换为天/时/分/秒
const days = Math.floor(remainingTime / (1000 * 60 * 60 * 24))
const hours = Math.floor(remainingTime % (1000 * 60 * 60 * 24) / (1000 * 60 * 60))
// ...继续转换分钟和秒
Date.now()
或new Date().getTime()
获取当前时间戳- 时间戳单位是毫秒(1秒=1000毫秒)
- 时间戳计算是倒计时、时长统计的核心基础