JS事件流
事件流
@jarringslee
文章目录
事件流是时间完整执行过程的流动路径
大到小:事件捕获;小到大:事件冒泡
事件捕获
目标: 简单了解事件捕获执行过程
-
概念: 从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() { ... }// 解绑方式:直接赋值为 nullbtn.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(事件类型,事件处理函数,捕获或冒泡阶段)
- 匿名函数无法被解绑
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.scrollTop console.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.clientWidth console.log(w) })//检查某一元素高度const div = document.querySelecotor(\'div\')console.log(div.clientHeight)
获取元素的尺寸与位置
-
获取尺寸:宽和高
获取元素自身的宽和高的可视数值,包括自身设置的宽高、padding、border,如果盒子是隐藏的,那么获取的结果将会是0。
属性名:
offseWidth
offseHeight
-
获取位置:
获取元素自己定位父级元素的左、上的距离(会算上元素的外边距等),注意这个值只读。
- 如果该元素没有父元素或者父元素没有设置定位属性,那么该元素的值是相对于浏览器窗口的位置;
- 如果该元素有父级元素并且父级元素有定位(父元素设置相对定位),那么该元素的值是自己相对于父元素边界的值。
属性名:
offseLeft
offseTop
scrollTop
clientHeight
offsetHeight
offsetTop
“导航栏在滑动到一定高度时显示/隐藏”小案例
const ele = document.querySelector(\'.xtx-elevator\') const entryh = document.querySelector(\'.xtx_entry\')//给浏览器窗口添加滚动事件 window.addEventListener(\'scroll\', function () { //检测窗口滚动量 let n = document.documentElement.scrollTop console.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.scrollTop if (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.scrollTop document.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 (中国标准时间)
日期对象方法
日期对象返回的数据无法直接使用,需要转换为实际开发中常用的格式
输出月份和星期时需要+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毫秒)
- 时间戳计算是倒计时、时长统计的核心基础