主题
图片加载失败事件与过渡事件(Day15 4/4)
本页对应 Day15_DOM,共 5 段视频,约 29 分钟。
学习目标
- 能为图片元素监听
error事件,并在加载失败时替换为指定的占位图片,验证页面不再显示失败图。 - 能说明
transitionstart、transitionrun、transitionend的触发时机与区别,并通过控制台输出验证延迟场景下的触发顺序。 - 能解释过渡事件触发次数与参与过渡属性数量的关系,并能结合元素过渡效果进行验证。
- 能为元素监听
animationstart、animationend、animationiteration,说明延迟、执行次数、交替与无限动画对事件触发的影响。 - 能为元素和
window监听scroll事件,获取并输出滚动距离,验证滚动时数值变化。 - 能为
window监听resize事件,实时获取并输出视口宽度和高度,验证调整浏览器窗口尺寸时数据更新。
知识点
先看清这次作业的要求
本节笔记只有两条:键盘事件控制元素位置移动、轮播图。这两件事表面上是“做效果”,实际上考的全是事件。转写稿这一节并没有直接写作业代码,而是把作业要用到的事件系统完整讲了一遍:图片加载失败、过渡、动画、滚动、视口尺寸变化。先把这几个事件吃透,作业才有下手的地方。
图片加载失败事件:给轮播图兜底
为什么需要:轮播图里的图片常常直接引用别人服务器上的地址(盗链),对方一旦删图,你的页面就只剩一个破图图标,非常难看。
怎么想:每一张 img 都可能是坏的,所以要给每个 img 单独监听,而不是监听外层容器——坏的是某一张图,不是整个盒子。
怎么做:先把结构搭出来,用 flex 让图片自动换行,再给每张图绑定 error:
html
<div class="wrapper">
<img src="images/DB001.jpg" alt="示例图片一">
<img src="images/DB002.jpg" alt="示例图片二">
<img src="images/随便写的错误地址.jpg" alt="示例图片三">
</div>css
.wrapper { width: 1000px; margin: 0 auto; display: flex; flex-wrap: wrap; }
.wrapper img { width: 220px; height: 220px; margin: 10px; }js
(function () {
const imageItems = document.querySelectorAll('.wrapper img');
imageItems.forEach(function (imageItem) {
imageItem.addEventListener('error', function () {
imageItem.src = 'images/暂无图片.png'; // 换成一张确定存在的图
});
});
})();刷新后,地址写错的那张就会显示“暂无图片”。
易错点:替换用的那张图必须确定存在,否则它自己也会触发 error,形成死循环;另外 error 只在加载失败时触发,不要拿它当 load 用。
过渡事件:轮播图切换的基础
为什么需要:轮播图切换靠的是 CSS transition,但“切换完再改文案、解锁按钮”这类后续动作,JS 必须知道过渡什么时候开始、什么时候结束。
怎么想:过渡发生在元素自己身上,所以事件绑在元素上,不绑 window。
怎么做:准备一个方块,点击切换类名,靠类名改样式触发过渡:
css
#book { width: 200px; height: 200px; background: #900; transition: 2s; }
#book.active { border-radius: 50%; background: #099; transform: rotate(720deg); }js
const book = document.querySelector('#book');
book.onclick = function () { book.classList.toggle('active'); };
book.ontransitionstart = function () { console.log('transition start'); };
book.ontransitionend = function () { console.log('transition end'); };
book.ontransitionrun = function () { console.log('transition run'); };调试过程:点一下,控制台各打印了 6 次。原因是:border-radius 有四个角各算一个属性,加上背景色、transform,一共 6 个属性在过渡,有几个属性过渡就触发几次。
易错点:transitionrun 和 transitionstart 都表示开始,区别是前者在延迟之前触发,后者在延迟之后触发;没设延迟时两者几乎同时触发。三者中 transitionend 用得最多,因为“等过渡结束再处理”是刚需。
动画事件:轮播图自动播放的基础
为什么需要:自动轮播一般用 animation,需要知道播到第几轮、什么时候整体播完。
怎么做:
css
@keyframes bookScroll {
to { transform: translate(1000px, 200px) rotate(3600deg); }
}
#book {
width: 100px; height: 100px; background: #900; color: #fff;
font-size: 30px; text-align: center; line-height: 100px;
animation: bookScroll 2s 1s; /* 时长 2s,延迟 1s */
}js
book.onanimationstart = function () { console.log('animation start'); };
book.onanimationend = function () { console.log('animation end'); };
book.onanimationiteration = function () { console.log('animation iteration'); };调试过程:加上 animation-iteration-count: 2 和 animation-direction: alternate 后再观察——start 在延迟结束后只触发一次,end 是全部动画结束才触发,iteration 则每执行一次触发一次。把次数改成无限次,animationend 永远不会触发。
易错点:想“每次播完做点事”要用 animationiteration,不要用 animationend;无限动画下 animationend 就是死的。
滚动事件:监听内容滚了多少
为什么需要:做吸顶、回到顶部、懒加载,以及轮播图的滚动联动,都要知道滚了多少。
怎么做:元素滚动监听自身,用 scrollTop 取滚动距离:
css
#book { width: 600px; height: 250px; padding: 20px; background: #099; overflow: auto; }
#book p { padding: 10px; background: #ccc; }js
const book = document.querySelector('#book');
book.onscroll = function () { console.log('内容滚动了', book.scrollTop); };
window.onscroll = function () {
console.log('页面滚动了', document.documentElement.scrollTop);
};易错点:window 上没有 scrollTop,页面滚动距离要取 document.documentElement.scrollTop。这是 W3C 给的特殊写法,记住即可,不必纠结为什么是 html 元素。
视口尺寸变化事件:响应式轮播图
为什么需要:窗口一改,轮播图宽度、每屏张数都要重算。
js
window.onresize = function () {
console.log(window.innerWidth, window.innerHeight);
console.log(document.documentElement.clientWidth, document.documentElement.clientHeight);
};易错点:resize 只能绑在 window 上;回调里别做重活,否则拖动窗口会卡。
回到作业本身
键盘控制移动:需要持续监听键盘事件,再配合元素的定位属性改 left / top。本节转写稿没有展开键盘事件的具体写法,作业时自行查阅 keydown / keyup 与定位的配合。
轮播图:把上面几个事件串起来——切换动画用 transition,并用 transitionend 做收尾;自动播放用 animation,用 animationiteration 数轮次;图片用 error 兜底;窗口变化用 resize 重算宽度。这样,这一节的每个事件都在作业里有落点。
课堂画面





代码示例
1. 图片加载失败事件(error)
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>图片加载失败事件</title>
<style>
.wrapper {
width: 1000px;
margin: 0 auto;
display: flex;
flex-wrap: wrap;
}
.wrapper img {
width: 220px;
height: 220px;
margin: 10px;
}
</style>
</head>
<body>
<div class="wrapper">
<img src="images/DB01.jpg" alt="">
<img src="images/DB02.jpg" alt="">
<img src="images/DB03.jpg" alt="">
<img src="不存在的地址/DB04.jpg" alt="">
<img src="随便胡写/DB05.jpg" alt="">
</div>
<script>
(function () {
// 1. 拿到所有需要判断的图片元素(NodeList)
var imageItems = document.querySelectorAll('.wrapper img');
// 2. 遍历,给每一张图单独监听 error 事件
imageItems.forEach(function (imageItem) {
imageItem.addEventListener('error', function () {
// 3. 加载失败:换成一张“暂无图片”的兜底图
imageItem.src = 'images/no-image.png';
});
});
})();
</script>
</body>
</html>要点解释 / 易错点:
error事件不冒泡,所以不能只在.wrapper上做事件委托,必须对每个<img>单独addEventListener('error', ...)。- 用立即执行函数包裹,避免变量污染全局。
- 转写稿里说的是“给图片设置
display: flex让它们自动换行”,实际上flex要加在容器上(.wrapper),并配合flex-wrap: wrap才能换行;给img自己设display: flex是无效的。 - 兜底图
images/no-image.png必须是自己确定存在的本地图;如果兜底图也加载失败会再次触发error,形成死循环,可加一次性判断(如判断当前src已是兜底图就不再替换)。 - 这种处理主要用在引用了别人网站图片(可能被删)的场景,自己服务器上的图片一般不需要。
2. 过渡事件(transitionrun / transitionstart / transitionend)
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>过渡事件</title>
<style>
#book {
width: 200px;
height: 200px;
background: #900;
/* 2 秒过渡,不设延迟:start 与 run 基本同时触发 */
transition: all 2s;
}
/* 想观察 start 与 run 的区别,可改成:transition: all 2s 3s; */
#book.active {
border-radius: 50%;
background: #099;
transform: rotate(720deg);
}
</style>
</head>
<body>
<div id="book"></div>
<script>
var book = document.querySelector('#book');
// 点击切换类名,CSS 过渡同样能被 JS 触发
book.onclick = function () {
book.classList.toggle('active');
};
// 过渡开始(有延迟时,延迟之后才触发)
book.addEventListener('transitionstart', function () {
console.log('transition start');
});
// 过渡开始(有延迟时,延迟之前就触发)
book.addEventListener('transitionrun', function () {
console.log('transition run');
});
// 过渡结束
book.addEventListener('transitionend', function (e) {
console.log('transition end', e.propertyName);
});
</script>
</body>
</html>要点解释 / 易错点:
- 一次过渡会触发多次事件:有几个属性参与过渡就触发几次。本例
border-radius算 4 个角 +background+transform= 6 次,所以控制台会打印 6 组。 transitionrun在延迟之前触发,transitionstart在延迟之后触发;没有设置延迟时两者几乎同时触发,容易误以为写重了。- 判断是哪个属性过渡结束,用事件对象的
e.propertyName。 - 实际用得最多的是
transitionend:等过渡真正结束再做后续处理,避免“过渡到一半就改样式”。 - 事件绑定用
addEventListener,不要写成book.ontransitionend = ...同时还想绑多个;on写法后写会覆盖前面。
3. 动画事件(animationstart / animationiteration / animationend)
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>动画事件</title>
<style>
#book {
width: 100px;
height: 100px;
background: #900;
color: #fff;
font-size: 30px;
text-align: center;
line-height: 100px;
/* 名称 时长 延迟 次数 交替方向 */
animation: bookScroll 2s 1s 3 alternate;
}
@keyframes bookScroll {
to {
transform: translate(1000px, 200px) rotate(3600deg);
}
}
</style>
</head>
<body>
<div id="book">动画</div>
<script>
var book = document.querySelector('#book');
// 动画开始:延迟之后才触发
book.addEventListener('animationstart', function () {
console.log('animation start');
});
// 动画每执行一次触发一次
book.addEventListener('animationiteration', function () {
console.log('animation iteration');
});
// 整个动画全部结束才触发
book.addEventListener('animationend', function () {
console.log('animation end');
});
</script>
</body>
</html>要点解释 / 易错点:
- 三个事件的时机:
animationstart在延迟之后触发;animationiteration每执行一次触发一次(本例执行 3 次就打印 2 次 iteration,最后一次进入animationend);animationend要等整个动画全部结束。 - 如果把次数写成
infinite,animationend永远不会触发,只会有animationstart和不断的animationiteration。 animation简写里名称必须写对,且@keyframes名与animation中的名称要一致,否则动画不跑,事件也不会触发。- 事件名容易写错:是
animationiteration(不是animationiterator),是animationend(不是animationand)。
常见错误
- 现象:图片已加载失败,但
error回调不执行。→ 原因:先赋src后监听,或脚本执行时图片已失败。→ 修法:先绑定error再赋src;对已存在图片检查img.complete && img.naturalWidth === 0后手动兜底,并加标记防止占位图再次失败导致循环。 - 现象:一次过渡触发多次
transitionend,收尾逻辑重复执行。→ 原因:未过滤event.target/event.propertyName,且过渡属性多或事件冒泡。→ 修法:回调中判断目标元素和目标属性,必要时用once或去重标记。 - 现象:设置过渡延迟后,以为
transitionstart会立刻触发。→ 原因:混淆transitionrun与transitionstart的触发时机。→ 修法:延迟前逻辑放transitionrun,真正开始放transitionstart,收尾放transitionend。 - 现象:无限动画的
animationend永远不执行,后续逻辑卡住。→ 原因:监听了animationend,但动画iteration-count: infinite。→ 修法:改用animationiteration计数、定时器或取消动画后处理,不要依赖animationend。 - 现象:滚动监听拿不到距离或始终为 0。→ 原因:监听
window却读容器scrollTop,或读错滚动主体。→ 修法:容器滚动读container.scrollTop;页面滚动读window.scrollY或document.documentElement.scrollTop,统一封装。 - 现象:
scroll/resize回调高频执行导致卡顿。→ 原因:未节流/防抖,回调内频繁读写布局。→ 修法:scroll用requestAnimationFrame或节流,resize用防抖,缓存尺寸并避免布局抖动。
练习
- 图片兜底:给一组
img实现加载失败显示占位图,要求已失败图片也能处理、占位图失败不递归,并说明监听顺序。 - 过渡事件:实现卡片点击后变圆、变色、旋转,记录
transitionrun/transitionstart/transitionend次数,并按propertyName只对目标属性做收尾。 - 动画事件:动画执行 3 次并交替运动,用
animationiteration输出轮次,animationend后恢复初始状态;再改为无限动画并给出收尾方案。 - 滚动与尺寸:分别实现容器滚动和页面滚动距离读取;窗口
resize防抖输出视口宽高,要求避免高频卡顿。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 图片加载失败事件 | 05:43 |
| 2 | 过渡事件 | 09:38 |
| 3 | 动画事件 | 06:18 |
| 4 | 滚动事件 | 05:09 |
| 5 | 视口尺寸变化事件 | 02:13 |
小结
- 事件监听顺序比事件名更关键,图片
error和动态资源尤其要先监听后触发。 - 过渡/动画事件天然可能多次触发,必须按
target、propertyName、轮次等条件过滤。 - 有延迟时区分
transitionrun与transitionstart;收尾优先用transitionend,无限动画不能依赖animationend。 - 滚动距离的来源必须与滚动主体一致,页面滚动和容器滚动不要混用。
resize只监听window,高频事件必须防抖/节流,避免回调内做重布局。- 占位兜底要有状态标记,防止备用资源失败后递归触发。
- 工程中应封装通用监听与清理逻辑,避免业务层散落重复事件处理。