Skip to content

2-7回车发布评论案例 ​

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

学习目标 ​

  1. 能够为文本域注册 keyup 键盘事件,通过事件对象 e.key 判断用户是否按下 Enter 键,并在控制台打印验证判断结果。
  2. 能够用 display: flex(而非 block)显示隐藏的评论列表项,并说明为何必须保留 flex 布局;能够用 innerHTML 将文本域的 value 写入评论内容容器,并验证写入含标签的内容时标签被解析生效。
  3. 能够在回车分支内完成表单复原:将文本域 value 置为空字符串、把字数统计文本恢复为初始的 “0/200”,并说明这两步为何必须写在回车判断条件内部。
  4. 能够为发布按钮注册 click 事件实现同样的发布功能,并在回车回调中通过 btn.click() 复用发布逻辑,避免重复代码。
  5. 能够按“谁调用函数,this 就指向谁”的规则,判断并验证 this 在全局环境、普通函数、对象方法、事件回调四种场景下分别指向 window、window、该对象、事件源。
  6. 能够用 this 替代具体变量名改写事件回调中的样式操作代码(如 this.style.backgroundColor),并说明这样写在变量重命名时无需修改回调代码的优势。

知识点 ​

为什么需要事件解绑 ​

事件绑定之后,事件处理函数会在每次触发时执行。但有些场景并不需要一直执行,例如“按钮只允许点击一次”“引导提示只弹一次”“提交后不再重复提交”。如果一直保留监听,用户再次点击还会进入回调,可能造成重复提示、重复提交或逻辑混乱。
所以,事件解绑的核心目的就是:把已经不需要的事件处理函数从事件源上移除。本节属于“了解”内容,重点掌握两种绑定方式对应的两种解绑写法。

先想清楚:解绑到底解什么 ​

事件绑定本质上是把某个函数登记到事件源上,等事件触发时再由事件源调用这个函数。转写稿里讲“谁调用函数,this 就指向谁”,也说明事件回调函数是由事件源调用的。解绑并不是删除按钮,也不是删除事件类型,而是把当初登记的那个函数引用移除掉。
因此要先判断绑定方式:

  • 用 addEventListener 绑定,就用 removeEventListener 解绑;
  • 用 onclick 这种 DOM0 方式绑定,就用 onclick = null 解绑。

思路是:先绑定事件,在回调函数中完成业务,再在合适的时机解绑。本节代码的需求是“按钮就点击一次,然后移除点击事件”,所以解绑写在点击回调内部。

两种解绑写法 ​

完整示例代码如下:

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('click', fn) 绑定,回调是命名函数 fn。第一次点击时执行 alert('我点击了'),随后 l2.removeEventListener('click', fn) 把同一个 fn 移除。之后再点 l2,不会再进入 fn。
l0 使用 l0.onclick = function () { ... } 绑定,解绑时直接写 l0.onclick = null。第一次点击仍会执行回调,执行到 null 后,原来的点击处理函数就被清除了。

易错点与调试 ​

第一,removeEventListener 必须传入同一个函数引用。如果绑定写 addEventListener('click', function () {}),解绑时再写一个匿名函数,两个函数不是同一个,无法移除。本节先用 function fn() {} 再传 fn,就是为了保证引用一致。
第二,事件类型写 'click',不要写成 'onclick'。onclick 是 DOM0 的属性名,addEventListener/removeEventListener 里用的是事件类型。
第三,不要写成 removeEventListener('click', fn()),fn() 会立即调用函数,应传函数本身 fn。
第四,DOM0 解绑是 l0.onclick = null,不是字符串 'null'。
第五,验证时可以按代码逻辑操作:第一次点 l2 会弹窗,关闭后再点第二次不再弹窗;l0 同理。如果第二次仍然弹窗,优先检查回调引用是否一致、事件名是否写错,以及是否误把匿名函数用于解绑。转写稿中出现的“实验对象/世界对象”应纠正为“事件对象”,“回调函数”也要理解为由事件源调用的函数;解绑时移除的正是这个回调函数。

先理解“其他事件”的调试思路 ​

这一节讲的是页面加载、元素滚动、页面尺寸三类“其他事件”。它们共同点是:先注册事件,再在回调函数里做业务处理。转写稿里做回车发布评论时,并不是一上来就写完整逻辑,而是先给文本域注册keyup,在回调里console.log(e.key),确认能拿到Enter,再写if判断;调试时还会把无关的log注释掉,避免干扰。这里顺手纠正同音字:转写稿中的“时间对象/世界对象”都应写作“事件对象”。这个调试思路可以迁移到本节:先监听、先打印验证触发时机,再判断和处理。显示评论模块时,转写稿还提醒不能把原本的display:flex随意改成block,否则布局会被破坏——这告诉我们,事件回调里改样式要保留原布局。

页面加载事件:load ​

为什么需要: 加载外部资源(如图片、外联CSS和JavaScript等)加载完毕时触发的事件。有些时候需要等页面资源全部处理完了再做一些事情;老代码喜欢把script写在head中,这时候直接找DOM元素找不到。

怎么想: 先判断你要等的是“所有资源都完成”,还是“HTML解析完就行”。如果要等图片、外链CSS、JS等都处理完,就用load。

怎么做: 监听页面所有资源加载完毕:

javascript
window.addEventListener('load', function() {
    // xxxxx
})

易错点: load等待资源多,触发较晚;不要把它和“HTML解析完成”混为一谈。回调里再去找DOM,通常更稳。

页面加载事件:DOMContentLoaded ​

为什么需要: 当初始的HTML文档被完全加载和解析完成之后就触发,而无需等待样式表、图像等完全加载。它适合“DOM已经能操作,但不必等图片”的场景。

怎么想: 如果业务只依赖DOM结构,不依赖图片尺寸等资源,优先考虑DOMContentLoaded,它通常比load更早。

怎么做:

javascript
document.addEventListener('DOMContentLoaded', function() {
    // xxxxx
})

易错点: 它不等图片等资源,所以别在回调里立刻读取依赖图片加载完成的尺寸;事件目标也不同:load常监听window,DOMContentLoaded监听document。

元素滚动事件:scroll ​

为什么需要: 滚动条在滚动的时候持续触发的事件。很多网页需要检测用户把页面滚动到某个区域后做一些处理,比如固定导航栏、返回顶部。

怎么想: 监听滚动 → 读取被卷去的距离 → 判断阈值 → 做处理。scrollTop / scrollLeft表示被卷去的头部或者左侧,可以读取,也可以修改(赋值)。

怎么做: 监听整个页面滚动:

javascript
window.addEventListener('scroll', function() {
    // xxxxx
})

易错点: scroll会持续高频触发,回调里不要堆重活;读页面滚动距离和读某个元素内部滚动距离要区分对象;scrollTop可读可改,修改赋值可用于回到某个位置。

页面尺寸事件:resize ​

为什么需要: 会在窗口尺寸改变的时候触发事件。页面宽度变化时,常需要重新调整布局或读取可见尺寸。

怎么想: 窗口变化 → 读取元素可见宽高 → 按新尺寸处理。clientWidth和clientHeight,获取元素的可见部分宽高(不包含border、margin、滚动条等)。

怎么做:

javascript
window.addEventListener('resize', function() {
    // xxxxx
})

易错点: resize同样可能高频触发,别在回调里做过多DOM操作;clientWidth/clientHeight不包含border、margin、滚动条等,别和含边框的宽高混用。调试时像转写稿那样先打印验证,再写业务判断,能减少“事件没触发/触发时机不对”的误判。

课件图示 ​

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

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

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

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

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

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

课件第8页:1.2 事件捕获 l 事件捕获需要写对应代码才能看到效果 l 代码: l 说明:

课件第9页:1.3 事件冒泡 事件冒泡概念: 当一个元素的事件被触发时,同样的事件将会在该元

代码示例 ​

1. 事件解绑(移除事件处理函数) ​

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>事件解绑</title>
</head>
<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>
</html>

关键行与易错点

  • removeEventListener('click', fn):必须传入同一个具名函数 fn,传匿名函数无法解绑(每次都是新函数,找不到要移除的那个)。
  • L0 事件(onclick)解绑只需把属性重新赋值为 null,l0.onclick = null。
  • L2 与 L0 的区别:addEventListener 可以绑多个同名事件,所以解绑要指定“解哪个”;onclick 只能绑一个,赋值即覆盖。

2. 页面加载 / 滚动 / 尺寸改变事件 ​

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>其他事件</title>
  <style>
    body { margin: 0; height: 2000px; background: linear-gradient(#fff, #333); }
  </style>
</head>
<body>
  <script>
    // 1. load:页面所有资源(图片、外联CSS、JS等)加载完毕
    window.addEventListener('load', function () {
      console.log('load:所有资源加载完毕')
    })

    // 2. DOMContentLoaded:HTML文档加载和解析完成即触发,无需等待样式表、图像
    document.addEventListener('DOMContentLoaded', function () {
      console.log('DOMContentLoaded:HTML文档加载和解析完成')
    })

    // 3. scroll:监听整个页面滚动
    window.addEventListener('scroll', function () {
      // scrollTop / scrollLeft:被卷去的头部或左侧,可以读取,也可以赋值修改
      console.log('scroll:被卷去的头部', document.documentElement.scrollTop)
    })

    // 4. resize:窗口尺寸改变时触发
    window.addEventListener('resize', function () {
      // clientWidth / clientHeight:元素可见部分宽高(不包含 border、margin、滚动条等)
      console.log('resize:可视宽度', document.documentElement.clientWidth)
    })
  </script>
</body>
</html>

关键行与易错点

  • load 要等所有资源处理完;DOMContentLoaded 只等 HTML 解析完,触发更早。老代码把 script 写在 head 中找不到 DOM 元素,就可以用这两个事件包裹。
  • scroll 事件注册在 window 上监听的是整个页面滚动,滚动条持续滚动会持续触发,回调里不要写太重的逻辑。
  • scrollTop 既可读也可写(赋值可做“返回顶部”),读取页面滚动位置用 document.documentElement.scrollTop。
  • clientWidth / clientHeight 拿的是可见部分宽高,不含 border、margin、滚动条。

3. 回车发布评论案例(含 this 指向事件源) ​

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>回车发布评论</title>
  <style>
    .wrapper { width: 400px; margin: 50px auto; }
    textarea { width: 100%; height: 100px; resize: none; box-sizing: border-box; }
    .total { text-align: right; color: #999; font-size: 12px; }
    button { display: block; margin: 10px auto; }
    /* 评论模块默认隐藏;显示时必须用 flex,保留它原本的 flex 布局 */
    .item { display: none; margin-top: 20px; }
    .item .avatar { width: 40px; height: 40px; border-radius: 50%; background: #ccc; margin-right: 10px; }
    .item .text { flex: 1; margin: 0; }
  </style>
</head>
<body>
  <div class="wrapper">
    <textarea class="tx" placeholder="请输入评论内容"></textarea>
    <div class="total">0/200字</div>
    <button>发布</button>

    <div class="item">
      <div class="avatar"></div>
      <p class="text"></p>
    </div>
  </div>

  <script>
    // 1. 获取元素
    const tx = document.querySelector('.tx')
    const total = document.querySelector('.total')
    const item = document.querySelector('.item')
    const text = document.querySelector('.text')
    const btn = document.querySelector('.wrapper button')

    // 2. 输入时统计字数
    tx.addEventListener('input', function () {
      total.innerHTML = tx.value.length + '/200字'
    })

    // 3. 需求3.1:键盘弹起,判断是否按了回车
    tx.addEventListener('keyup', function (e) {
      if (e.key === 'Enter') {
        // 按回车 = 自动点一下发布按钮(复用发布逻辑,不用再复制一份代码)
        btn.click()
      }
    })

    // 4. 需求4:点击发布按钮发布评论
    btn.addEventListener('click', function () {
      if (tx.value.trim() === '') {
        alert('请输入内容')
        return
      }
      // 4.1 显示评论模块:用 flex,不能用 block,否则 flex 布局失效
      item.style.display = 'flex'
      // 4.2 把文本域的内容写到评论盒子(用 innerHTML 以支持标签,如 <strong>)
      text.innerHTML = tx.value
      // 4.3 清空文本域
      tx.value = ''
      // 4.4 复原字数
      total.innerHTML = '0/200字'

      // 事件回调中的 this 指向事件源,这里 this 就是 btn
      // this.style.background = 'pink'
    })
  </script>
</body>
</html>

关键行与易错点

  • 判断回车用事件对象 e.key === 'Enter',键盘事件用 keyup(输入完成才触发)。
  • 显示评论模块用 item.style.display = 'flex':它本身是 flex 布局,改成 block 会让头像和文字竖着排,布局就坏了。
  • 写内容用 text.innerHTML = tx.value,支持用户输入 <strong> 之类的标签;若只想纯文本可用 innerText。
  • 清空与复原必须写在“发布成功”之后:tx.value = '' 清空文本域,total.innerHTML = '0/200字' 复原字数,两件事缺一不可。
  • 按回车时不要再复制一遍四句代码,直接 btn.click() 模拟点击发布按钮,代码更简洁。
  • this 的粗略规则:谁调用这个函数,this 就指向谁。事件回调里 this 指向事件源(按钮);普通函数和全局里 this 指向 window;对象方法里 this 指向该对象。

常见错误 ​

  1. 现象:调用 removeEventListener 后事件仍然触发。
    原因:绑定时传入匿名函数,解绑时又写了另一个匿名函数,引用不一致。
    修法:把回调提取为具名函数,绑定和解绑使用同一个函数引用。

  2. 现象:评论模块显示后,图片和文字变成上下排列。
    原因:把原本 display: flex 的容器直接改成 display: block,破坏了原有布局。
    修法:显示时恢复为 display: flex,不要随意改成 block。

  3. 现象:按回车后评论出现了,但文本域没清空、字数没复原。
    原因:清空文本域和复原字数的代码写在了 if 判断外面或执行顺序不对。
    修法:把清空文本域、复原字数统计的代码放进“确认按下回车”的分支内。

  4. 现象:点击发布按钮能发评论,按回车不能,或二者行为不一致。
    原因:回车分支和按钮点击分支各写一套逻辑,后续修改遗漏其中一处。
    修法:统一发布逻辑;回车时可触发发布按钮的 click(),避免重复维护两套代码。

  5. 现象:事件回调里用 this 获取不到预期元素,或指向 window。
    原因:使用了箭头函数,或没有理解当前调用者;this 指向受调用方式影响。
    修法:需要指向事件源时使用普通函数;必要时改用 e.currentTarget 明确获取绑定元素。

  6. 现象:脚本写在 head 中直接查询 DOM,得到 null。
    原因:DOM 尚未解析完成就执行了查询。
    修法:使用 DOMContentLoaded 等待 DOM 解析完成,或把脚本放到合适位置;需等全部资源时再用 load。

练习 ​

  1. 改写“只点击一次”的按钮:分别用 addEventListener/removeEventListener 和 onclick = null 实现,并说明解绑成功的关键条件。

  2. 给评论案例增加“发布成功后清空并复原字数”的完整流程:写出回车发布与点击发布按钮共用逻辑的改法。

  3. 一个容器原本是 display:flex,默认隐藏。请写出点击后显示该容器且不破坏内部横排布局的代码。

  4. 写一段代码:页面滚动时读取被卷去的头部距离,滚动超过 300px 时给导航栏添加固定定位样式。

  5. 判断下列场景中 this 的指向并说明理由:全局普通函数调用、对象方法调用、按钮点击回调中使用普通函数、按钮点击回调中使用箭头函数。

原视频对照 ​

#视频时长
1回车发布评论案例-上11:21
2回车发布评论案例-下05:45
3环境对象this09:58

小结 ​

  1. 解绑事件的核心是“绑谁解谁”,尤其 removeEventListener 必须使用同一函数引用。
  2. 显示隐藏元素时不要破坏原有布局,display 应恢复到元素原本需要的值。
  3. 回车发布评论的关键是:判断回车键、显示评论、写入内容、清空文本域、复原字数。
  4. 回车发布和按钮发布应尽量复用同一套逻辑,减少重复代码和遗漏修改。
  5. this 是环境对象,当前阶段可按“谁调用函数,this 指向谁”粗略判断。
  6. 事件回调中常用 this 指向事件源,但箭头函数会改变这一判断方式。
  7. 页面加载、滚动、尺寸变化事件要按需求选择 load、DOMContentLoaded、scroll、resize。
  8. 读取滚动距离和可见尺寸时,要区分 scrollTop/scrollLeft 与 clientWidth/clientHeight 的用途。