当前位置: 首页 > news >正文

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('我是儿子') })  

执行过程说明:

  1. 点击 son 元素时,会依次触发:
    • son 的点击事件 → 弹出 “我是儿子”
    • father 的点击事件 → 弹出 “我是爸爸”
    • document 的点击事件 → 弹出 “我是爷爷”
  2. 事件从触发元素向外(父级)逐层传播,形成冒泡。

鼠标经过事件:

  • 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 会依次触发 sonfather 的事件。

注意事项

  • stopPropagation() 不仅阻止冒泡阶段,在捕获阶段也同样有效。
  • 适用于需要精确控制事件传播的场景,如模态框、下拉菜单等。

事件解绑

L0 解绑方式

// 绑定事件
btn.onclick = function() { ... }// 解绑方式:直接赋值为 null
btn.onclick = null

特点:直接覆盖原始事件处理函数

L2 解绑方式 (addEventListener)

// 绑定事件
function handleClick() { ... }
btn.addEventListener('click', handleClick)// 解绑方式:使用相同参数
btn.removeEventListener('click', handleClick)

关键要求

  1. 事件类型相同
  2. 必须是同一个函数引用
  3. 捕获阶段需与绑定时一致

匿名函数和箭头函数无法被解绑

// 匿名函数无法解绑
btn.addEventListener('click', function() { ... })// 箭头函数无法解绑(匿名特性)
btn.addEventListener('click', () => { ... })

原因
解绑需要函数引用,匿名函数无法被二次引用

  1. 需要解绑的事件 → 使用具名函数 + L2方式
  2. 一次性事件 → 在函数内使用removeEventListener自解绑
  3. 不需要解绑 → 可用匿名函数

两种注册事件方法

  • 传统on注册(L0)

    • 同一个对象,后面注册的事件会覆盖前面注册的事件(同一个事件)
    • 直接使用null覆盖就可以实现事件的解绑
    • 都是冒泡阶段执行的
  • 事件监听注册(L2)

    • 语法:addEventListener(事件类型,事件处理函数,是否使用捕获)
    • 后面注册的事件不会覆盖前面注册的事件(同一个事件)
    • 可以通过第三个参数控制冒泡或捕获阶段执行
    • 必须使用removeEventListener(事件类型,事件处理函数,捕获或冒泡阶段)
    • 匿名函数无法被解绑
特性传统 on 注册(L0)事件监听注册(L2)
语法element.on事件 = 处理函数addEventListener(事件类型, 处理函数, 是否捕获)
重复绑定同事件后绑定的会覆盖前绑定的同事件可绑定多个,按注册顺序依次执行
解绑方式element.on事件 = nullremoveEventListener(事件类型, 处理函数, 捕获阶段)
事件阶段仅冒泡阶段执行可通过第三个参数控制捕获(true)或冒泡(false)阶段
匿名函数处理可直接覆盖解绑匿名函数无法解绑(需使用具名函数引用)
  1. 覆盖性:L0 会覆盖同名事件,L2 不会。
  2. 灵活性:L2 支持捕获/冒泡阶段控制,L0 仅冒泡。
  3. 解绑要求:L2 需严格匹配参数(尤其是函数引用),L0 直接赋 null 即可。
  4. 需要精细控制事件阶段或多函数绑定 → 优先用 L2
  5. 简单场景或快速解绑需求 → 可用 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毫秒)
  • 时间戳计算是倒计时、时长统计的核心基础
http://www.dtcms.com/a/297674.html

相关文章:

  • 疯狂星期四第19天运营日记
  • 网络资源模板--基于Android Studio 实现的天气预报App
  • LeetCode 127:单词接龙
  • 三维图像识别中OpenCV、PCL和Open3D结合的主要技术概念、部分示例
  • 水库大坝安全监测的主要内容
  • MySQL 全新安装步骤(Linux版yum源安装)
  • Lua(面向对象)
  • 深度学习水论文:特征提取
  • NBIOT模块 BC28通过MQTT协议连接到EMQX
  • 如何在 Ubuntu 24.04 或 22.04 上安装和使用 GDebi
  • 智能网关:物联网时代的核心枢纽
  • ABP VNext + Razor 邮件模板:动态、多租户隔离、可版本化的邮件与通知系统
  • 智能网关芯片:物联网连接的核心引擎
  • 酷暑来袭,科技如何让城市清凉又洁净?
  • 制造业低代码平台实战评测:简道云、钉钉宜搭、华为云Astro、金蝶云·苍穹、斑斑低代码,谁更值得选?
  • 使用 FFmpeg 实现 RTP 音频传输与播放
  • 【Redis】初识Redis(定义、特征、使用场景)
  • Spring框架
  • 认识编程(3)-语法背后的认知战争:类型声明的前世今生
  • vue3单页面连接多个websocket并实现断线重连功能
  • 机器学习笔记(三)——决策树、随机森林
  • Git指令
  • git将本地文件完和仓库文件目录完全替换-------还有将本地更新的文件放到仓库中,直接提交即可
  • C# WPF 实现读取文件夹中的PDF并显示其页数
  • STM32与ADS1220多通道采样数据
  • vscode 登录ssh记住密码直接登录设置
  • GPU 服务器ecc报错处理
  • 详谈OSI七层模型和TCP/IP四层模型以及tcp与udp为什么是4层,http与https为什么是7层
  • SQL 查询与自定义管理工具设计:释放数据底层价值
  • linux C — udp,tcp通信