主题
常用事件总结(Day15 3/4)
本页对应 Day15_DOM,共 5 段视频,约 1 小时 4 分。
学习目标
- 能够为
form元素监听submit和reset事件,说明其触发条件、监听对象,并演示表单提交与重置时的回调执行。 - 能够为表单控件监听
focus和blur事件,实现获取焦点时显示“正在输入”、失去焦点时显示“输入完毕”的交互效果。 - 能够区分
input与change事件的触发时机,并分别用于输入框实时内容变化和选择框选项改变场景。 - 能够使用
selectedIndex、change事件和options.length = 0完成省市地址联动,并能在初始化时手动调用回调显示默认省份对应城市。 - 能够监听图片
load和error事件,利用已加载图片数量与总图片数量计算加载进度,更新进度条和百分比,并在加载完成后隐藏遮罩层。 - 能够说明
select事件的触发条件,并监听输入框或文本域中文本被选中时的行为。
知识点
先建立一张“事件地图”
本节把常用事件按对象分成几类:表单元素上的 submit/reset,表单控件上的 focus/blur/select/input/change,以及图片元素上的 load/error。学习时不要只背事件名,要先问:这个事件监听在谁身上、什么时候触发、触发后我通常要做什么。转写稿反复强调“监听”和“方法”不是一回事:blur() 是主动让元素失焦的方法,onblur 是用户失焦时被浏览器调用的回调。
表单提交与重置:submit / reset
为什么需要:提交和重置是表单最基础的动作,后面做校验、阻止默认提交都要从这里入手。
怎么想:提交可由点击提交按钮触发,也可在表单内按回车触发;重置由重置按钮触发。它们都应监听在 form 元素上,而不是按钮上。
怎么做:转写稿演示的关键代码可整理为:
html
<form action="地址">
<input type="email" placeholder="请输入邮箱"><br>
<input type="password" placeholder="请输入密码"><br>
<button>提交</button>
<button type="reset">重置</button>
</form>js
const formBox = document.querySelector('form');
formBox.onsubmit = function () {
alert('我正在被提交');
};
formBox.onreset = function () {
alert('我正在被重置');
};调试时先写邮箱密码,点提交会弹框;弹框会暂停程序,点确定后表单仍会继续提交。也就是说,仅仅监听 submit 不能阻止默认提交,阻止默认行为要等事件对象相关内容再讲。按回车提交同样能触发 submit;点重置也会触发 reset,确定后重置继续执行。
易错点:submit/reset 监听在 form 上;按钮默认是提交,重置按钮要写 type="reset";不要误以为弹框后默认动作就停了。
焦点与失焦:focus / blur
为什么需要:输入框获得焦点表示准备输入,失去焦点常被当作“这一段写完了”,所以 blur 很适合做实时校验。
怎么想:只有表单控件通常有焦点,事件应监听输入框等控件。需求可设为:聚焦时在 span 显示“正在输入”,失焦时显示“输入完毕”。
怎么做:
js
const inputBox = document.querySelector('input');
const spanBox = document.querySelector('span');
inputBox.onfocus = function () {
spanBox.innerText = '正在输入';
};
inputBox.onblur = function () {
spanBox.innerText = '输入完毕';
};调试时鼠标点进输入框,span 变“正在输入”;再点别处,变“输入完毕”。转写稿还提到实际表单常在 blur 时做邮箱格式实时验证:用户写完邮箱去点密码框,就检查邮箱格式。
易错点:blur 事件和 blur() 方法不是一回事;焦点事件不要监听到 form 上,要监听具体控件。
文本选中:select
为什么需要:需要知道输入框或文本域里的文字是否被鼠标选中时可使用。
怎么想:它监听在输入框、文本域这类有文本内容的元素上。
怎么做:
js
inputBox.onselect = function () {
alert('我里面的内容被选中了');
};调试时在输入框写几个字,用鼠标拖选文字,松手触发。
易错点:select 事件不是下拉框的 change;它用于文本内容被选中,使用频率不高。
内容变化:input 与 change
为什么需要:要实时获取输入内容,或在下拉框、单选复选变化时做联动。
怎么想:input 是输入框内容一改就触发,能覆盖粘贴、剪切等纯鼠标操作,比只靠 keyup 更稳;change 对输入框要求“内容改变且失去焦点”,对选择类元素则一改变就触发。
怎么做:
js
inputBox.oninput = function () {
console.log(inputBox.value);
};
inputBox.onchange = function () {
console.log(inputBox.value);
};调试 change:只输入不失焦不触发;只失焦不改变也不触发;既改变又失焦才触发。对复选框:
js
checkBox.onchange = function (e) {
console.log(e.target.checked);
};点一下复选框就触发。
易错点:change 不适合做输入框实时监听;选择类元素(单选、复选、下拉、日期、颜色等)改变即触发。
地址联动:change + selectedIndex + 清空
为什么需要:省市联动是 change 的典型应用。
怎么想:两个 select,省数组和市数组按索引对应;省选项变化后,用 selectedIndex 找到当前省索引,再取出对应城市数组。
怎么做:先遍历省数组,用 select.add(new Option(省名, index)) 添加选项;给省 select 监听 change,取 selectedIndex,遍历城市数组添加到市 select。
易错点:每次追加城市前必须清空,否则会不断叠加。转写稿演示 citySel.options.length = 0 可清空。首次默认选中第一个省不会触发 change,可手动调用一次回调,让默认城市也显示。
图片加载:load / error
为什么需要:游戏官网等常用高清大图做开场加载进度。文档的 load 只在整页加载完触发一次,不能反映过程;图片的 load 表示图片加载完毕,error 表示失败(地址错、网络故障等),abort 现在基本不用研究。
怎么想:影响加载速度的常是高清大图。把高清大图地址放入数组,为每个地址创建 new Image() 并监听 onload;相同 src 浏览器只下载一次,可复用缓存。每触发一次 load,已加载数量加一,进度为“已加载数 / 图片总数”。
怎么做:调试时打开 Network 面板,选高速 3G/低速 3G 刷新,观察进度条和百分比;加载完成后把遮罩 mask 隐藏或 top:-100%,配合 transition 上移。
易错点:不要以为监听整页 load 能拿到过程;进度判断要基于高清大图;回调触发顺序不可控,所以用计数器而不是假定第一张先完成。
本节范围说明
本节笔记只列了两项作业:“键盘事件控制元素位置移动”“轮播图”。但提供的转写稿实际讲的是表单事件、地址联动和图片加载事件,并没有键盘控制移动与轮播图的实现过程。为遵守“不得编造”,下面按转写稿整理教程正文;键盘移动和轮播图需另取课堂素材再补。
表单提交与重置事件
为什么需要:表单提交和重置是表单最基础的行为。监听它们,才能在用户提交或重置时执行校验、提示等逻辑。
怎么想:提交可由提交按钮触发,也可在表单控件中按回车触发;重置由重置按钮触发。事件要监听在 form 元素上,而不是按钮上。
怎么做:
html
<form action="/submit">
<input type="email" placeholder="请输入邮箱"><br>
<input type="password" placeholder="请输入密码"><br>
<button>提交</button>
<button type="reset">重置</button>
</form>js
const formBox = document.querySelector('form');
formBox.onsubmit = function () {
alert('我正在被提交');
};
formBox.onreset = function () {
alert('我正在被重置');
};调试过程:点击提交出现弹窗;弹窗会暂停当前程序,但点确定后表单仍会继续提交。监听 submit 并不会自动阻止表单默认提交。重置同理,点确定后重置继续执行。
易错点:button 默认是提交按钮;重置按钮必须写 type="reset"。submit、reset 监听在 form 上,不要监听错元素。
焦点事件与实时验证
为什么需要:输入框获取焦点说明用户准备输入,失去焦点通常说明用户写完了。利用 focus 和 blur 可以做“正在输入/输入完毕”提示,也可做实时校验。
怎么想:focus 是获取焦点,blur 是失去焦点;它们和“让元素失焦”的方法不是一回事,事件是“行为发生时被监听到”。
怎么做:
js
const inputBox = document.querySelector('input');
const spanBox = document.querySelector('span');
inputBox.onfocus = function () {
spanBox.innerHTML = '正在输入';
};
inputBox.onblur = function () {
spanBox.innerHTML = '输入完毕';
};调试过程:鼠标点进输入框,提示“正在输入”;点页面空白处失焦,提示“输入完毕”。实际开发中常见做法是:邮箱写完后点密码框,此时邮箱输入框失焦,立即提示“邮箱格式不正确”。
易错点:焦点事件监听在表单控件上,不是 form 上;blur 事件很常用于表单实时验证,但真正校验格式通常还要配合正则。
input 与 change 的区别
为什么需要:要实时获取输入框内容,常用 input;要监听选择类控件变化,常用 change。
怎么想:input 是输入框内容一改变就触发;change 对输入框要求“内容改变且失去焦点”,对单选框、复选框、下拉框则是改变即触发。
怎么做:
js
inputBox.oninput = function () {
console.log(inputBox.value);
};
inputBox.onchange = function () {
console.log(inputBox.value);
};调试过程:keyup 在键盘输入时能实时取值,但纯鼠标粘贴时可能不触发;input 在输入、粘贴、剪切、删除时都能实时反映内容。change 监听输入框时,只改内容不失焦不触发;既改内容又失焦才触发。监听复选框时,点击改变选中状态就触发。
易错点:不要以为 change 和 input 一样实时;input 更适合输入框和文本域,change 更适合选择类控件。
地址联动:change 的典型用法
为什么需要:选择省后,市的下拉项要跟着变化,这是 change 的典型场景。
怎么想:先根据省数组生成省选项;省下拉改变时,取 selectedIndex,再用它取对应城市数组;追加城市前必须先清空旧选项。
怎么做:
js
const provinceSel = document.querySelector('#province');
const citySel = document.querySelector('#city');
provinceList.forEach((item, index) => {
const op = document.createElement('option');
op.value = index;
op.textContent = item;
provinceSel.add(op);
});
provinceSel.onchange = function () {
citySel.options.length = 0;
const index = provinceSel.selectedIndex;
cityList[index].forEach(item => {
const op = document.createElement('option');
op.textContent = item;
citySel.add(op);
});
};
provinceSel.onchange();调试过程:第一次只显示省;选择浙江后城市出现。若不先清空,会不断追加,出现“安徽也有杭州、江苏也有杭州”。用 citySel.options.length = 0 可清空旧选项。首次默认选中第一项但没触发 change,所以手动调用一次 provinceSel.onchange()。
易错点:忘记清空;忘记首次手动执行;把 selectedIndex 当成选项文本。
图片加载事件与加载进度
为什么需要:网页中的高清大图会明显拖慢加载。监听图片 load,可在图片加载完后更新进度,全部完成后隐藏遮罩。
怎么想:不直接监听页面已有图片,而是把高清大图地址放入数组,遍历创建 Image 元素并设置 src;相同图片浏览器只下载一次,可用缓存。每张图 load 后计数加一,计算百分比。
怎么做:
js
const maskBox = document.querySelector('#maskBox');
const progressContent = document.querySelector('.progress-content');
const progressNumber = document.querySelector('.progress-number');
let loadedImages = 0;
imgList.forEach(src => {
const img = new Image();
img.onload = function () {
loadedImages++;
const progress = loadedImages / imgList.length;
progressContent.style.width = progress * 100 + '%';
progressNumber.innerHTML = progress * 100 + '%';
if (progress >= 1) {
maskBox.style.top = '-100%';
}
};
img.src = src;
});调试过程:在开发者工具“网络”里把网速调成高速 3G,刷新后可看到百分比从 0 逐步上升;全部图片加载完后遮罩上移。error 表示图片加载失败。
易错点:不要给所有小图标都做进度判断,通常只判断高清大图;load 是图片元素加载完毕,不是整个文档 load;进度条宽度和数字都要更新。
作业提醒
本节笔记中的“键盘事件控制元素位置移动”和“轮播图”在转写稿中没有出现,不能凭空补代码。建议先按上面表单事件与图片加载案例完成练习,再结合后续素材补作业实现。
课堂画面





代码示例
代码一:表单事件全家桶(submit / reset / focus / blur / select / input / change)
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>表单事件:submit / reset / focus / blur / select / input / change</title>
<style>
input.text {
width: 300px;
padding: 5px;
border: 1px solid #999;
outline: none; /* 去掉输入框默认的焦点轮廓 */
}
span { padding-left: 20px; }
</style>
</head>
<body>
<form action="http://www.itcast.cn">
<input type="email" placeholder="请输入邮箱"><br>
<input type="password" placeholder="请输入密码"><br>
<button>提交</button>
<button type="reset">重置</button>
</form>
<hr>
<input type="text" class="text" placeholder="请输入内容">
<span>等待输入</span>
<br><br>
<input type="checkbox" class="check"> 我同意
<script>
// 1. 表单提交 / 重置:必须监听在 form 元素上
var formBox = document.querySelector('form');
formBox.onsubmit = function () {
alert('我正在被提交');
};
formBox.onreset = function () {
alert('我正在被重置');
};
// 2. 焦点事件:监听在表单控件元素上(不是 form 上)
var inputBox = document.querySelector('.text');
var spanBox = document.querySelector('span');
inputBox.onfocus = function () { // 获取焦点
spanBox.innerHTML = '正在输入';
};
inputBox.onblur = function () { // 失去焦点
spanBox.innerHTML = '输入完毕';
};
// 3. select:输入框里的文字被鼠标选中时触发
inputBox.onselect = function () {
alert('我里面的内容被选中了');
};
// 4. input:内容一改就实时触发;change:内容改变且失去焦点才触发
inputBox.oninput = function () {
console.log('input:' + this.value);
};
inputBox.onchange = function () {
console.log('change:' + this.value);
};
// 5. change 监听在选择类控件上:一改变就触发,不需要失去焦点
var checkBox = document.querySelector('.check');
checkBox.onchange = function () {
console.log('checked:' + this.checked);
};
</script>
</body>
</html>要点解释 / 易错点
onsubmit、onreset只能加在form元素上;focus、blur、select、input、change要加在表单控件上。- 表单提交不只有点“提交”按钮一种方式,焦点在表单里按回车也能提交,同样会触发
onsubmit。 - 弹窗会暂停当前程序,点“确定”后表单照样会提交——只写
alert是阻止不了默认提交的(想阻止需要在回调里return false,后续讲事件对象时再说)。 blur事件和blur()方法不是一回事:事件是“你失去焦点我来监听”,方法是“我主动让你失去焦点”。input是实时的(键盘、粘贴、剪切、删除都能触发),change用在输入框上必须“内容改变 且 失去焦点”才触发,所以不适合输入框,适合单选/复选/下拉等选择类控件。
代码二:地址联动(select + change + selectedIndex + options.length = 0)
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>地址联动</title>
</head>
<body>
<select id="prov"></select>
<select id="city"></select>
<script>
(function () {
// 两个数组:第一个存省,第二个存市,靠索引一一对应
var provList = ['江苏', '浙江', '安徽', '河南'];
var cityList = [
['南京', '苏州', '无锡'],
['杭州', '宁波', '温州'],
['合肥', '芜湖', '蚌埠'],
['郑州', '洛阳', '开封']
];
var provSel = document.querySelector('#prov');
var citySel = document.querySelector('#city');
// 第一步:根据数据把“省”的选项加进去
provList.forEach(function (item, index) {
var op = document.createElement('option'); // 只能造 option 元素
op.innerHTML = item;
op.value = index; // 用索引当 value,方便后面取数组
provSel.add(op);
});
// 第二步:省发生改变 → 把对应的市加进来
provSel.onchange = function () {
var index = this.selectedIndex; // 当前选中项的索引
citySel.options.length = 0; // ★ 先清空,否则会不停追加
cityList[index].forEach(function (item, i) {
var op = document.createElement('option');
op.innerHTML = item;
op.value = i;
citySel.add(op);
});
};
// 第三步:手动调用一次回调,让页面一打开就有默认城市
provSel.onchange();
})();
</script>
</body>
</html>要点解释 / 易错点
- 两个数组靠索引对应:
provList[2]是安徽,cityList[2]就是安徽的城市。 select元素上的change非常好触发,选项一变就触发,不像输入框还要失去焦点。select的selectedIndex属性可以拿到当前选中项的索引,正好当数组下标用。- 最容易错的一步:往第二个下拉框
add之前一定要先清空(citySel.options.length = 0,或citySel.innerHTML = ''),否则每选一次就追加一次,最后会出现两个杭州、三个合肥。 - 页面第一次打开时并没有“改变”,
change不会触发,所以默认省份的城市出不来——手动调用一次provSel.onchange()即可(本质就是把对象上那个方法调一遍)。
代码三:图片加载完毕 load 事件 + 网页加载进度条
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>图片加载完毕事件 load —— 网页加载进度条</title>
<style>
* { margin: 0; padding: 0; }
/* 遮罩层:一打开就盖住整个视口 */
.mask-box {
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100%;
background: #000;
transition: top 1s; /* 配合 JS 改 top,做“滑上去”的效果 */
}
.mask-inner {
width: 60%;
margin: 300px auto;
}
.progress-box {
border: 1px solid #fff;
height: 20px;
}
.progress-content {
width: 0%; /* 默认 0%,由 JS 改宽度 */
height: 20px;
background: #fff;
}
.progress-number {
color: #fff;
margin-top: 10px;
text-align: center;
}
img { display: block; width: 500px; margin: 20px auto; }
</style>
</head>
<body>
<!-- 遮罩 + 进度条 -->
<div class="mask-box" id="maskBox">
<div class="mask-inner">
<div class="progress-box">
<div class="progress-content"></div>
</div>
<div class="progress-number">0%</div>
</div>
</div>
<!-- 页面里的高清大图(地址换成你自己的图片即可) -->
<img src="https://picsum.photos/id/1015/1200/800">
<img src="https://picsum.photos/id/1016/1200/800">
<img src="https://picsum.photos/id/1018/1200/800">
<img src="https://picsum.photos/id/1025/1200/800">
<script>
(function () {
// 1. 获取相关元素
var maskBox = document.querySelector('#maskBox');
var progressContent = maskBox.querySelector('.progress-content');
var progressNumber = maskBox.querySelector('.progress-number');
// 2. 把页面里高清大图的地址存进数组(不用去获取页面上的 img 元素)
var imgList = [
'https://picsum.photos/id/1015/1200/800',
'https://picsum.photos/id/1016/1200/800',
'https://picsum.photos/id/1018/1200/800',
'https://picsum.photos/id/1025/1200/800'
];
// 3. 记录已加载完成的图片数量
var loadedImage = 0;
// 4. 遍历地址,每张图创建一个新的 img 元素并监听 load
imgList.forEach(function (item) {
var imageEle = document.createElement('img');
imageEle.src = item; // 设置 src 就会去请求,不必插入页面
imageEle.onload = function () { // 这张图下载完了
loadedImage++;
var progress = loadedImage / imgList.length; // 计算当前进度
progressContent.style.width = progress * 100 + '%'; // 进度条变化
progressNumber.innerHTML = progress * 100 + '%'; // 百分比数字变化
if (progress >= 1) { // 全部加载完毕
maskBox.style.top = '-100%'; // 配合 transition 滑走
}
};
imageEle.onerror = function () { // 地址错 / 网络故障
console.log('图片加载失败:' + item);
};
});
})();
</script>
</body>
</html>要点解释 / 易错点
- 文档的
load只在整页加载完毕时触发一次,靠它做不了进度;做进度靠的是每张图片元素自己的load。 - 这里监听的是 JS 新建的 img 元素,不是页面上已有的 img。同一个图片地址浏览器只会下载一次并缓存,所以不会重复下载、不会拖慢速度。
- 新创建的 img 元素只要设置了
src就会去请求,不需要 append 到页面。 loadedImage++必须放在onload回调里,哪张先下载完就先触发,顺序不可控,所以进度要用“已加载数 ÷ 总数”实时算。- 遮罩的隐藏用
top: -100%配合 CSS 的transition: top 1s,JS 只改属性值,过渡效果交给 CSS。 - 想看清进度条效果:F12 → Network → 把网络调成“高速 3G / 低速 3G”再刷新。
- 记得加
onerror,地址写错或网络异常时进度永远到不了 100%,遮罩会一直盖着。
常见错误
- submit/reset 无反应或逻辑不触发 → 事件绑到了按钮/input 上,而不是 form 上 → 改绑 form,并确认按钮
type正确。 - 以为 submit 回调执行完就能阻止提交 → 未处理浏览器默认提交行为 → 把“业务校验”和“阻止默认行为”分开处理,按课程后续事件对象方法处理。
- 把 blur 事件和
input.blur()方法混用 → 事件监听与方法调用概念混淆,或把 focus/blur 绑到 form/span 上 → 事件绑到表单控件,方法调用单独写。 - 输入框监听 change 却期待逐字实时变化 → change 在输入框上通常要“内容改变且失焦”才触发 → 实时输入用 input,选择类控件再用 change。
- 地址联动切换省份后城市不断追加 → 渲染前没有清空旧 option → 每次先清空城市下拉,再按
selectedIndex渲染。 - 地址联动首次打开城市为空 → 初始化时没有触发一次渲染逻辑 → 页面加载后手动执行一次渲染函数或初始化流程。
- 图片加载进度不更新或重复绑定 → 监听对象/图片地址管理混乱,或事件重复绑定 → 用高清图地址数组创建 Image,监听 load 并计数,统一更新进度。
- 键盘控制移动时元素越界、页面滚动、按住不连续 → 只处理 keydown、未记录按键状态、未判断边界、未处理方向键默认滚动 → keydown/keyup 维护状态,更新位置时做边界判断并处理默认行为。
- 轮播图越播越快或索引错乱 → 多个定时器叠加、索引越界、鼠标移入未清理定时器 → 单定时器管理,索引循环取模,移入清除、移出重启。
练习
- 写一个含邮箱、密码、提交和重置按钮的表单:form 监听 submit/reset,分别输出提示;再用回车提交验证一次。
- 输入框实现 focus 显示“正在输入”、blur 显示“输入完毕”;再给输入框加 select 事件,选中文本时输出提示。
- 同一个输入框分别监听 input 和 change,记录触发次数;用逐字输入、粘贴、剪切、失焦四种操作对比触发情况。
- 完成省市级联:要求首次打开有默认城市、切换省份先清空旧城市、用
selectedIndex对应数组索引、禁止重复追加。 - 完成作业二选一的基础版并说明事件设计:A. 键盘控制元素移动(含边界与按住连续移动);B. 轮播图(含单定时器、索引循环、移入移出处理)。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 表单事件-提交和重置 | 06:12 |
| 2 | 表单事件-获取焦点和失去焦点 | 08:51 |
| 3 | 表单事件-change和input事件 | 08:40 |
| 4 | 地址联动案例 | 14:33 |
| 5 | 图片加载完毕事件 | 25:58 |
小结
- 先确认“事件绑在谁身上”,再写回调;表单类事件尤其要先检查绑定对象。
- 业务校验与浏览器默认行为要分开设计,不能用弹窗代替提交控制。
- 实时输入、选择变化、失焦校验是不同场景,事件选择必须匹配场景。
- 联动渲染遵循“先清空、再按索引渲染、最后做初始化”的顺序。
- 图片加载进度应由图片 load 成功计数与总数驱动 UI,完成后清理遮罩。
- 键盘移动要维护按键状态,并处理边界与方向键默认滚动。
- 轮播图要单定时器、索引循环、移入移出清理,避免重复绑定和定时器叠加。
- 调试事件问题优先查:绑定对象、触发条件、默认行为、是否重复绑定。