Skip to content

2-6统计用户字数案例 ​

本页对应 第二章WebAPIs,共 6 段视频,约 52 分钟。

学习目标 ​

  • 能使用 focus、blur 为表单元素绑定获得/失去焦点事件,并能用 元素.focus() 实现页面加载后自动获得焦点。
  • 能独立实现小米搜索框案例:通过焦点事件控制下拉菜单显示/隐藏,并使用 classList.add/remove 切换搜索框边框样式。
  • 能区分 keydown、keyup、input 的触发时机、执行顺序及取值差异,说明 keydown 为何拿不到最后一次按键值。
  • 能使用 input 事件与 value.length 完成用户输入字数统计,并通过焦点事件控制统计提示的显示与隐藏。
  • 能说明事件对象来自事件回调函数的第一个参数,并能使用 e.key 判断回车键、使用 e.target、e.offsetX/Y 获取事件源或鼠标坐标。
  • 能复述事件流的基本概念,并了解使用 removeEventListener 解绑事件的基本写法与适用场景。

知识点 ​

为什么要学习事件流? ​

为什么需要: 初学事件时,最常见的疑惑是“我明明只点了子盒子,为什么父盒子的点击事件也弹了一次?”这背后就是事件流。事件流指的是事件完整执行过程中的流动路径。不理解它,就很难解释弹窗两次、事件委托、阻止冒泡等问题。

怎么想: 把一次点击想成一次“从外到里,再从里到外”的传递。触发事件时会经历两个阶段:捕获阶段、冒泡阶段。捕获是从DOM根元素开始依次调用同名事件,也就是从外到里;冒泡则相反,从当前元素向上调用祖先元素的同名事件。

事件流捕获与冒泡示意图

怎么做: 先用addEventListener注册事件,再通过第三个参数控制阶段:传true走捕获,传false或不传走冒泡。实际开发以冒泡为主,捕获很少使用。

易错点: 不要以为“点击子盒子只触发子盒子”。默认存在冒泡,所以子元素事件会向上影响父级;调试时可在回调里console.log(e.target)看真正触发元素,也可先点子盒子观察弹窗顺序。

捕获阶段 ​

为什么需要: 捕获能让我们理解事件从根元素向目标元素传递的过程,也能解释为什么给父元素加true后,点击子元素会先触发父元素。

怎么想: 捕获需要写对应代码才能看到效果,语法是:

html
元素.addEventListener('click', 回调函数, 是否使用捕获)

怎么做: 第三个参数传true代表捕获阶段触发。

html
<body>
  <div class="father">
    父盒子
    <div class="son">子盒子</div>
  </div>
  <script>
    // 事件流
    const father = document.querySelector('.father')
    const son = document.querySelector('.son')
    // 1. 事件捕获
    // // 点击父盒子
    father.addEventListener('click', function () {
      alert('我是爸爸')
    }, true)  // 事件捕获
    // 点击子盒子
    son.addEventListener('click', function () {
      alert('我是儿子')
    }, true) // 事件捕获
  </script>
</body>

易错点: true是捕获,false是冒泡且为默认值;捕获阶段很少用于业务,不要为了“看起来高级”而滥用。调试时若发现先弹“爸爸”再弹“儿子”,说明捕获生效了。

冒泡阶段 ​

为什么需要: 冒泡是默认存在的事件流动方式,也是实际工作中最常用的机制。点击子盒子会依次向上调用所有父级元素的同名事件,所以会出现“子盒子点击,父盒子也响应”的现象。

怎么想: 当一个元素触发事件后,同样的事件会在该元素的所有祖先元素中依次被触发。简单理解:子元素触发后,会依次向上调用父级元素的同名事件。

怎么做: 不传第三个参数或传false,都是冒泡。

html
<body>
  <div class="father">
    父盒子
    <div class="son">子盒子</div>
  </div>
  <script>
    // 事件流
    const father = document.querySelector('.father')
    const son = document.querySelector('.son')
    // 2. 事件冒泡
    // 点击父盒子
    father.addEventListener('click', function () {
      alert('我是爸爸')
    })
    // 点击子盒子
    son.addEventListener('click', function () {
      alert('我是儿子')
    }, false) 
  </script>
</body>

易错点: 点击子盒子弹出两次,不是代码写错,而是冒泡导致父元素也被触发;alert会阻塞页面,调试多个事件时容易看不清顺序,可改用console.log观察。

阻止冒泡 ​

为什么需要: 默认冒泡会让子元素事件影响到父级或祖先元素,若只想让事件限制在当前元素内,就要阻止事件冒泡。

怎么想: 阻止冒泡需要拿到事件对象。转写稿里也强调:事件对象是事件回调函数的第一个参数,里面包含事件触发相关信息,常命名为e、ev或event。

怎么做: 在子元素回调中接收e,调用e.stopPropagation()。

html
<body>
  <div class="father">
    父盒子
    <div class="son">子盒子</div>
  </div>
  <script>
    // 事件流
    const father = document.querySelector('.father')
    const son = document.querySelector('.son')
    // 1. 事件冒泡
    // 点击父盒子
    father.addEventListener('click', function () {
      alert('我是爸爸')
    })
    // 点击子盒子
    son.addEventListener('click', function (e) {
      alert('我是儿子')
      // 1.1 先获取事件对象
      // 1.2 事件对象.stopPropagation()  注意是个方法 
      e.stopPropagation()
    }) 
  </script>
</body>

易错点: stopPropagation是方法,必须加括号调用;不要写成属性赋值。事件对象必须从回调参数拿,不能凭空写e。另外,mouseover和mouseout会有冒泡效果,mouseenter和mouseleave没有冒泡效果,推荐后者。

事件委托 ​

为什么需要: 如果同时给多个元素注册事件,需要循环多次注册;大量事件监听比较耗费性能。事件委托可以把原本注册在子元素上的事件委托给父元素,让父元素担当事件监听职务,减少注册次数,提高程序性能。

怎么想: 事件委托利用事件冒泡的特点:给父元素注册事件,触发子元素时会冒泡到父元素,从而触发父元素的事件。再利用事件对象e.target得到真正触发事件的元素。

怎么做: 给父元素ul注册点击事件,判断e.target.tagName === 'LI'再操作。tagName得到的是大写标签名,所以比较时写'LI'。

html
<body>
  <ul>
    <li>第1个孩子</li>
    <li>第2个孩子</li>
    <li>第3个孩子</li>
    <li>第4个孩子</li>
    <li>第5个孩子</li>
  </ul>
  <script>
    // 需求: 点击每个小li都会有弹窗效果
    // 1. 获取父元素ul
    const ul = document.querySelector('ul')

    // 2. 给ul注册点击事件
    ul.addEventListener('click', function (e) {
      // alert('我会弹窗')
      // 3. 利用事件对象.target 得到目标元素
      // console.log(e.target)
      // e.target.style.color = 'pink'

      // 需求2:点击哪个小li,对应的li变色
      // console.dir(e.target.tagName) 可以得到目标元素的标签名
      if (e.target.tagName === 'LI') {
        e.target.style.color = 'pink'
      }
    })
  </script>

笔记中“大量注册”的示例用于说明性能问题:

html
<script>
  // 假设页面中有 10000 个 button 元素
  const buttons = document.querySelectorAll('table button');

  for(let i = 0; i <= buttons.length; i++) {
    // 为 10000 个 button 元素添加了事件
    buttons.addEventListener('click', function () {
      // 省略具体执行逻辑...
    })
  }
</script>

易错点: 这个示例本身有笔误:buttons是集合,不能直接buttons.addEventListener,实际应取buttons[i];循环条件也不应<= buttons.length,否则会越界。事件委托中不要直接操作this当目标元素,要用e.target;若父元素内部还有子标签,需判断真正目标或使用closest。

阻止默认行为 ​

为什么需要: 有些元素自带默认行为,例如点击提交按钮会提交表单,点击链接会跳转。业务中需要先校验或拦截时,就要阻止默认行为。

怎么想: 阻止默认行为同样依赖事件对象。它和阻止冒泡不同:stopPropagation阻止事件继续传播,preventDefault阻止元素默认动作。

怎么做: 在事件回调中调用e.preventDefault()。

html
<body>
  <form action="">
    姓名: <input type="text" name="username">
    <button>提交</button>
  </form>
  <a href="http://www.baidu.com">点击跳转</a>
  <script>
    // 阻止默认行为
    const form = document.querySelector('form')
    const input = document.querySelector('[name=username]')
    form.addEventListener('submit', function (e) {
      // 如果input表单的值为空则不允许提交
      if (input.value === '') {
        // return 无法阻止提交事件
        e.preventDefault()  // 阻止提交事件
      }
    })

    document.querySelector('a').addEventListener('click', function (e) {
      e.preventDefault()
    })
  </script>
</body>

易错点: 笔记中已注明return无法阻止提交事件;要阻止默认行为必须调用e.preventDefault()。同时不要把preventDefault和stopPropagation混用:前者管默认动作,后者管冒泡传播。

事件解绑:让事件只生效一次 ​

为什么需要: 有些按钮只允许点击一次,比如提交后不再重复提交、弹窗只提示一次。第一次点击执行完逻辑后,如果还把事件处理函数留在元素上,后续再点还会继续触发。事件解绑就是移除事件处理函数,让元素不再响应某个事件。

怎么想: 解绑的前提是“已经绑定”。转写稿里反复强调:事件要先获取元素,再给元素绑定事件,回调函数是事件触发后执行的函数。解绑同样围绕“元素、事件类型、回调函数”三件事展开。先确认元素是谁、绑定的是什么事件、处理函数是哪一个,再决定怎么移除。

怎么做: 本节用两个按钮对比两种写法:addEventListener 绑定的 L2 事件监听,用 removeEventListener 移除;onclick 赋值的 L0 事件监听,用 onclick = null 移除。注意,转写稿里“时间监听、回掉函数、帮定”都是口误,正确写法是“事件监听、回调函数、绑定”。

先看本节代码结构 ​

本节笔记没有附图片,核心就是下面这段代码。两个按钮分别演示 L2 和 L0 的解绑方式:

html
<body>
  <button class="l2">移除L2事件监听</button>
  <button class="l0">移除L0事件监听</button>
  <script>
    // 需求:按钮就点击一次,然后移除点击事件

    // 1. l2事件监听
    const l2 = document.querySelector('.l2')

    l2.addEventListener('click', fn)
    function fn() {
      alert('我点击了')
      // 移除事件监听
      l2.removeEventListener('click', fn)
    }

    // 2. l0事件监听
    const l0 = document.querySelector('.l0')
    l0.onclick = function () {
      alert('我点击了')
      // 移除事件监听
      l0.onclick = null
    }
  </script>
</body>

易错点: 解绑不是删除元素,也不是把页面上所有事件都清空;它只移除“指定元素、指定事件类型、指定处理函数”的那一个监听。

L2 事件监听的解绑 ​

为什么需要: addEventListener 可以给同一个元素绑定多个事件处理函数,所以移除时也要精确指定“移除哪一个”,不能笼统地说“移除点击事件”。

怎么想: removeEventListener 需要和 addEventListener 对应起来。代码里绑定的是 'click' 事件,处理函数是 fn,移除时也写 l2.removeEventListener('click', fn)。这里最关键的是:绑定和移除必须使用同一个函数引用。

怎么做: 先把处理函数声明为具名函数 fn,绑定和移除都传 fn。在 fn 内部先执行 alert,再执行移除。这样第一次点击会弹窗,弹窗结束后监听被移除,第二次点击就不会再触发。

易错点: 不能写成匿名函数再移除,因为两次写的匿名函数不是同一个引用;事件类型不要写成 'onclick',addEventListener 和 removeEventListener 里写 'click'。调试时可以借用转写稿里“先验证事件有没有绑定成功”的习惯:先点一次确认能弹窗,再点第二次确认不再弹窗;如果第二次还弹,就检查函数是不是同一个 fn、事件名是否一致。

L0 事件监听的解绑 ​

为什么需要: onclick 是给元素的事件属性赋值,同一个元素后一次赋值会覆盖前一次赋值。它的移除方式和 addEventListener 不同,不能直接套用 removeEventListener。

怎么想: 既然 onclick 是通过赋值绑定的,那么移除就是把它重新赋值为 null。代码里在回调函数内部执行 l0.onclick = null,表示这次点击处理完后,把点击属性清空。

怎么做: 用 l0.onclick = function () { ... } 绑定,函数内部先 alert,再写 l0.onclick = null。第一次点击执行函数,随后属性被置空;后续再点击,元素上没有 onclick 处理函数,就不会再执行。

易错点: onclick = null 只移除 L0 方式绑定的处理函数,不会影响通过 addEventListener 绑定的监听;反过来,removeEventListener 也不能用来移除 onclick 赋值的匿名函数。两种绑定方式要对应各自的解绑方式。

小结 ​

事件解绑的核心是“绑定和解绑要对应”:L2 用 addEventListener 绑定,就用 removeEventListener 移除,并且必须传同一个具名函数;L0 用 onclick 赋值绑定,就用 onclick = null 移除。本节转写稿主要讲焦点、键盘、input 和事件对象,没有单独展开解绑调试,所以这里不虚构额外过程,只按本节代码推导:第一次点击触发回调,回调内部执行解绑,第二次点击不再触发。

课件图示 ​

课件第10页:总结 总结 1. 事件流是什么?分为哪几个阶段? Ø 事件流指的是事件完整执行过

课件第26页:两种注册事件的区别 l 传统on注册(L0) Ø 同一个对象,后面注册的事件会覆

课件第6页:1.1 事件流 l 为什么要学习事件流? Ø 可以帮我们解决一些疑惑,比如点击子

课件第7页:1.1 事件流 事件流指的是事件完整执行过程中的流动路径 当触发事件时,会经历两

课件第11页:1.4 阻止冒泡 l 问题:因为默认就有冒泡阶段的存在,所以容易导致事件影响到父

课件第15页:l 事件委托是利用事件流的特征解决一些开发需求的知识技巧 Ø 优点:减少注册次数

课件第54页:描述 属性/方法 效果 事件流 addEventListener(‘事件类型’,

课件第25页:2. 移除事件监听(了解) 移除事件处理函数,也称为解绑事件 移除L2事件监听

代码示例 ​

示例 1:事件流 —— 捕获、冒泡与阻止冒泡 ​

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>事件流:捕获 / 冒泡 / 阻止冒泡</title>
  <style>
    .father {
      width: 300px;
      height: 300px;
      background-color: skyblue;
      padding: 20px;
      margin-bottom: 30px;
    }
    .son {
      width: 150px;
      height: 150px;
      background-color: pink;
      margin: 20px auto;
      line-height: 150px;
      text-align: center;
    }
  </style>
</head>
<body>
  <h3>1. 事件捕获(第三个参数 true):点儿子,先弹爸爸再弹儿子</h3>
  <div class="father father1">
    父盒子
    <div class="son son1">子盒子</div>
  </div>

  <h3>2. 事件冒泡(默认 false):点儿子,先弹儿子再弹爸爸</h3>
  <div class="father father2">
    父盒子
    <div class="son son2">子盒子</div>
  </div>

  <h3>3. 阻止冒泡:点儿子只弹儿子</h3>
  <div class="father father3">
    父盒子
    <div class="son son3">子盒子</div>
  </div>

  <script>
    // 1. 事件捕获:从 DOM 根元素开始,从外到里依次调用同名事件
    const father1 = document.querySelector('.father1')
    const son1 = document.querySelector('.son1')
    father1.addEventListener('click', function () {
      alert('我是爸爸(捕获)')
    }, true)
    son1.addEventListener('click', function () {
      alert('我是儿子(捕获)')
    }, true)

    // 2. 事件冒泡:从里到外,依次向上调用所有父级元素的同名事件(默认行为)
    const father2 = document.querySelector('.father2')
    const son2 = document.querySelector('.son2')
    father2.addEventListener('click', function () {
      alert('我是爸爸(冒泡)')
    })
    son2.addEventListener('click', function () {
      alert('我是儿子(冒泡)')
    }, false)

    // 3. 阻止冒泡:先拿到事件对象,再调用 stopPropagation()
    const father3 = document.querySelector('.father3')
    const son3 = document.querySelector('.son3')
    father3.addEventListener('click', function () {
      alert('我是爸爸(被阻止)')
    })
    son3.addEventListener('click', function (e) {
      alert('我是儿子(已阻止冒泡)')
      e.stopPropagation() // 方法,必须加小括号
    })
  </script>
</body>
</html>

关键行与易错点

  • addEventListener('click', fn, true) 第三个参数传 true 表示在捕获阶段触发;传 false 或省略表示冒泡阶段触发,实际开发以冒泡为主。
  • 捕获是“从外到里”,冒泡是“从里到外”,所以示例 1 先弹爸爸、示例 2 先弹儿子。
  • 阻止冒泡的前提是拿到事件对象:回调函数的第一个形参 e,然后调用 e.stopPropagation()。注意它是方法,忘写括号就没效果。
  • 易错:stopPropagation() 只阻止事件传播,不会阻止默认行为;阻止默认行为用的是 e.preventDefault(),两者不要混用。
  • 易错:onclick 方式同一元素只能绑定一个处理函数,后写的会覆盖前面的,推荐统一用 addEventListener。
  • 补充:mouseover / mouseout 有冒泡效果,mouseenter / mouseleave 没有冒泡效果(推荐)。

示例 2:事件委托 + 阻止默认行为 ​

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>事件委托 + 阻止默认行为</title>
  <style>
    ul { list-style: none; padding: 0; }
    li {
      width: 200px;
      height: 40px;
      line-height: 40px;
      border: 1px solid #ccc;
      margin-bottom: 6px;
      text-align: center;
      cursor: pointer;
    }
  </style>
</head>
<body>
  <ul>
    <li>第1个孩子</li>
    <li>第2个孩子</li>
    <li>第3个孩子</li>
    <li>第4个孩子</li>
    <li>第5个孩子</li>
  </ul>

  <form action="">
    姓名: <input type="text" name="username">
    <button>提交</button>
  </form>
  <a href="http://www.baidu.com">点击跳转</a>

  <script>
    // 事件委托:只给父元素 ul 注册一次点击事件
    const ul = document.querySelector('ul')
    ul.addEventListener('click', function (e) {
      // e.target 是真正触发事件的元素
      // console.dir(e.target.tagName) 可以得到目标元素的标签名
      if (e.target.tagName === 'LI') {
        e.target.style.color = 'pink'
      }
    })

    // 阻止默认行为:表单值为空则不允许提交
    const form = document.querySelector('form')
    const input = document.querySelector('[name=username]')
    form.addEventListener('submit', function (e) {
      if (input.value === '') {
        // return 无法阻止提交事件
        e.preventDefault() // 阻止提交事件
      }
    })

    // 阻止链接跳转
    document.querySelector('a').addEventListener('click', function (e) {
      e.preventDefault()
    })
  </script>
</body>
</html>

关键行与易错点

  • 事件委托的原理就是事件冒泡:给父元素注册事件,点击子元素时冒泡到父元素,从而触发父元素的处理函数,减少注册次数、提高性能。
  • 通过 e.target 拿到真正触发事件的元素,再用 e.target.tagName === 'LI' 判断点的是不是小 li。
  • 易错:tagName 返回的是大写标签名('LI'),写成 'li' 判断会永远不成立。
  • 易错:不要用循环给每个元素注册事件,且笔记中的循环示例本身有坑 —— buttons 是 querySelectorAll 得到的集合,集合上没有 addEventListener 方法;同时 i <= buttons.length 会越界,应为 i < buttons.length。
  • 阻止默认行为要用事件对象上的 e.preventDefault():表单空值时 return 并不能阻止提交,链接跳转同理。

示例 3:小米搜索框案例(焦点事件控制显示隐藏) ​

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>小米搜索框案例(焦点事件)</title>
  <style>
    * { margin: 0; padding: 0; box-sizing: border-box; }
    body { padding: 100px; }
    .mi { position: relative; width: 300px; }
    .search-text {
      width: 300px;
      height: 40px;
      padding: 0 10px;
      border: 1px solid #ccc;
      outline: none;
    }
    /* 获得焦点时给搜索框加上的类:橙色边框 */
    .search { border: 1px solid #ff6700; }
    /* 下拉菜单:绝对定位,初始隐藏 */
    .result-list {
      position: absolute;
      top: 40px;
      left: 0;
      width: 300px;
      border: 1px solid #ff6700;
      border-top: none;
      list-style: none;
      display: none;
    }
    .result-list li {
      height: 36px;
      line-height: 36px;
      padding-left: 10px;
      cursor: pointer;
    }
    .result-list li:hover { background-color: #f5f5f5; }
  </style>
</head>
<body>
  <div class="mi">
    <input type="search" class="search-text" placeholder="小米手机">
    <ul class="result-list">
      <li>小米手机</li>
      <li>小米电视</li>
      <li>小米手环</li>
      <li>小米笔记本</li>
    </ul>
  </div>

  <script>
    // 短横线类名在 JS 中不能直接做变量名,所以变量用下划线
    const searchText = document.querySelector('.search-text')
    const resultList = document.querySelector('.result-list')

    // 获得焦点:显示下拉菜单 + 搜索框变色
    searchText.addEventListener('focus', function () {
      resultList.style.display = 'block'
      searchText.classList.add('search')
    })

    // 失去焦点:隐藏下拉菜单 + 移除类名恢复原来的灰色边框
    searchText.addEventListener('blur', function () {
      resultList.style.display = 'none'
      searchText.classList.remove('search')
    })
  </script>
</body>
</html>

关键行与易错点

  • 第一步必须先在 CSS 中把下拉菜单 display: none 隐藏,这一步不能省略,否则页面一打开下拉菜单就露出来了。
  • 获得焦点事件是 focus,失去焦点事件是 blur;显示用 display = 'block',隐藏用 display = 'none'。
  • 边框变色采用“加类名 / 移除类名”的方式(classList.add('search')、classList.remove('search')),比直接改 style.border 更便于复用和还原。
  • 易错:search-text 这种带短横线的类名不能直接当变量名,变量名要改成 searchText。
  • 易错:元素.focus()(加小括号)是自动获得焦点的方法,和绑定 focus 事件是两回事;百度首页一打开搜索框就有光标,就是用了 元素.focus()。

常见错误 ​

  1. 点击子元素时父元素事件也执行,误以为绑错元素 → 事件默认冒泡到祖先元素 → 不需要父级响应时在子元素回调中调用 e.stopPropagation();需要批量处理时改用事件委托并判断 e.target。
  2. addEventListener 第三个参数 true/false 混用,导致父子执行顺序与预期相反 → 捕获与冒泡阶段选择不清 → 默认不传第三参数按冒泡处理;确需捕获时显式传 true,并固定团队约定。
  3. 阻止冒泡写成 e.stopPropagation 或漏写事件对象 → 把方法当属性、回调未接收 e → 写成 function (e) { e.stopPropagation() },并检查回调第一个参数是否存在。
  4. 事件委托中直接操作 e.target,点到 li 内 span 时样式或逻辑错乱 → target 是真正触发事件的最深元素 → 用 e.target.tagName、closest('li') 或 contains 判断并锁定目标子项。
  5. 搜索框失焦隐藏下拉,点击下拉项时下拉先消失 → blur 先触发,点击项未来得及响应 → 下拉项用 mousedown 并 preventDefault(),或延迟隐藏并做状态判断。
  6. keydown 中读取输入框 value 拿不到最后一次按键 → 按键值尚未写入表单值 → 需要完整值用 keyup 或 input;仅判断按键用 e.key。
  7. removeEventListener 解绑无效 → 绑定与解绑传入的不是同一函数引用 → 使用具名函数绑定,并用同一函数引用解绑;onclick 方式可置 null。
  8. 表单提交监听中用 return 阻止提交无效 → return 只结束当前回调,不取消默认行为 → 使用 e.preventDefault()。

练习 ​

  1. 用事件委托实现:点击 ul 下任意 li 变色;若点到 li 内的 span,也要让所在 li 变色。
  2. 实现卡片点击弹窗,但卡片内“删除”按钮点击时只执行删除逻辑,不触发卡片弹窗。
  3. 实现搜索框:聚焦显示下拉菜单并高亮边框,失焦隐藏并恢复边框;点击下拉项时菜单不能先消失。
  4. 实现留言统计:textarea 设置 maxlength="200",输入时实时显示“已输入 x/200 字”,页面初始也要显示“0/200”。
  5. 实现回车提交:在输入框中按回车触发提交提示,其他按键不触发;同时说明为什么不在 keydown 中依赖输入框最新 value 做完整校验。

原视频对照 ​

#视频时长
1焦点事件07:47
2小米搜索框案例10:05
3键盘和input事件10:05
4统计用户字数案例-上07:45
5统计用户字数案例-下05:14
6事件对象11:17

小结 ​

  1. 事件流默认走冒泡,捕获必须显式启用;排查父子触发顺序先看第三个参数。
  2. stopPropagation() 只解决传播问题,不应滥用来掩盖错误的事件绑定结构。
  3. 事件委托的核心是“父级监听 + 冒泡 + target 判断”,适合列表和批量元素。
  4. 焦点显隐类交互要重点处理 blur 与点击下拉项的时序冲突。
  5. keydown、keyup、input 的触发时机不同,取值完整性和按键判断要分开选事件。
  6. 事件对象是回调第一个参数,e.key 判断按键,e.target 判断真实触发元素。
  7. preventDefault() 管默认行为,stopPropagation() 管传播,二者职责不能混用。
  8. 解绑事件必须保证函数引用一致;匿名函数无法直接 removeEventListener。