主题
2-7回车发布评论案例
本页对应 第二章WebAPIs,共 3 段视频,约 27 分钟。
学习目标
- 能够为文本域注册
keyup键盘事件,通过事件对象e.key判断用户是否按下 Enter 键,并在控制台打印验证判断结果。 - 能够用
display: flex(而非block)显示隐藏的评论列表项,并说明为何必须保留 flex 布局;能够用innerHTML将文本域的value写入评论内容容器,并验证写入含标签的内容时标签被解析生效。 - 能够在回车分支内完成表单复原:将文本域
value置为空字符串、把字数统计文本恢复为初始的 “0/200”,并说明这两步为何必须写在回车判断条件内部。 - 能够为发布按钮注册
click事件实现同样的发布功能,并在回车回调中通过btn.click()复用发布逻辑,避免重复代码。 - 能够按“谁调用函数,this 就指向谁”的规则,判断并验证
this在全局环境、普通函数、对象方法、事件回调四种场景下分别指向 window、window、该对象、事件源。 - 能够用
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、滚动条等,别和含边框的宽高混用。调试时像转写稿那样先打印验证,再写业务判断,能减少“事件没触发/触发时机不对”的误判。
课件图示








代码示例
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指向该对象。
常见错误
现象:调用
removeEventListener后事件仍然触发。
原因:绑定时传入匿名函数,解绑时又写了另一个匿名函数,引用不一致。
修法:把回调提取为具名函数,绑定和解绑使用同一个函数引用。现象:评论模块显示后,图片和文字变成上下排列。
原因:把原本display: flex的容器直接改成display: block,破坏了原有布局。
修法:显示时恢复为display: flex,不要随意改成block。现象:按回车后评论出现了,但文本域没清空、字数没复原。
原因:清空文本域和复原字数的代码写在了if判断外面或执行顺序不对。
修法:把清空文本域、复原字数统计的代码放进“确认按下回车”的分支内。现象:点击发布按钮能发评论,按回车不能,或二者行为不一致。
原因:回车分支和按钮点击分支各写一套逻辑,后续修改遗漏其中一处。
修法:统一发布逻辑;回车时可触发发布按钮的click(),避免重复维护两套代码。现象:事件回调里用
this获取不到预期元素,或指向window。
原因:使用了箭头函数,或没有理解当前调用者;this指向受调用方式影响。
修法:需要指向事件源时使用普通函数;必要时改用e.currentTarget明确获取绑定元素。现象:脚本写在
head中直接查询 DOM,得到null。
原因:DOM 尚未解析完成就执行了查询。
修法:使用DOMContentLoaded等待 DOM 解析完成,或把脚本放到合适位置;需等全部资源时再用load。
练习
改写“只点击一次”的按钮:分别用
addEventListener/removeEventListener和onclick = null实现,并说明解绑成功的关键条件。给评论案例增加“发布成功后清空并复原字数”的完整流程:写出回车发布与点击发布按钮共用逻辑的改法。
一个容器原本是
display:flex,默认隐藏。请写出点击后显示该容器且不破坏内部横排布局的代码。写一段代码:页面滚动时读取被卷去的头部距离,滚动超过 300px 时给导航栏添加固定定位样式。
判断下列场景中
this的指向并说明理由:全局普通函数调用、对象方法调用、按钮点击回调中使用普通函数、按钮点击回调中使用箭头函数。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 回车发布评论案例-上 | 11:21 |
| 2 | 回车发布评论案例-下 | 05:45 |
| 3 | 环境对象this | 09:58 |
小结
- 解绑事件的核心是“绑谁解谁”,尤其
removeEventListener必须使用同一函数引用。 - 显示隐藏元素时不要破坏原有布局,
display应恢复到元素原本需要的值。 - 回车发布评论的关键是:判断回车键、显示评论、写入内容、清空文本域、复原字数。
- 回车发布和按钮发布应尽量复用同一套逻辑,减少重复代码和遗漏修改。
this是环境对象,当前阶段可按“谁调用函数,this指向谁”粗略判断。- 事件回调中常用
this指向事件源,但箭头函数会改变这一判断方式。 - 页面加载、滚动、尺寸变化事件要按需求选择
load、DOMContentLoaded、scroll、resize。 - 读取滚动距离和可见尺寸时,要区分
scrollTop/scrollLeft与clientWidth/clientHeight的用途。