Skip to content

图片加载失败事件与过渡事件(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

课堂画面 2

课堂画面 3

课堂画面 4

课堂画面 5

代码示例 ​

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)。

常见错误 ​

  1. 现象:图片已加载失败,但 error 回调不执行。→ 原因:先赋 src 后监听,或脚本执行时图片已失败。→ 修法:先绑定 error 再赋 src;对已存在图片检查 img.complete && img.naturalWidth === 0 后手动兜底,并加标记防止占位图再次失败导致循环。
  2. 现象:一次过渡触发多次 transitionend,收尾逻辑重复执行。→ 原因:未过滤 event.target / event.propertyName,且过渡属性多或事件冒泡。→ 修法:回调中判断目标元素和目标属性,必要时用 once 或去重标记。
  3. 现象:设置过渡延迟后,以为 transitionstart 会立刻触发。→ 原因:混淆 transitionrun 与 transitionstart 的触发时机。→ 修法:延迟前逻辑放 transitionrun,真正开始放 transitionstart,收尾放 transitionend。
  4. 现象:无限动画的 animationend 永远不执行,后续逻辑卡住。→ 原因:监听了 animationend,但动画 iteration-count: infinite。→ 修法:改用 animationiteration 计数、定时器或取消动画后处理,不要依赖 animationend。
  5. 现象:滚动监听拿不到距离或始终为 0。→ 原因:监听 window 却读容器 scrollTop,或读错滚动主体。→ 修法:容器滚动读 container.scrollTop;页面滚动读 window.scrollY 或 document.documentElement.scrollTop,统一封装。
  6. 现象:scroll / resize 回调高频执行导致卡顿。→ 原因:未节流/防抖,回调内频繁读写布局。→ 修法:scroll 用 requestAnimationFrame 或节流,resize 用防抖,缓存尺寸并避免布局抖动。

练习 ​

  1. 图片兜底:给一组 img 实现加载失败显示占位图,要求已失败图片也能处理、占位图失败不递归,并说明监听顺序。
  2. 过渡事件:实现卡片点击后变圆、变色、旋转,记录 transitionrun / transitionstart / transitionend 次数,并按 propertyName 只对目标属性做收尾。
  3. 动画事件:动画执行 3 次并交替运动,用 animationiteration 输出轮次,animationend 后恢复初始状态;再改为无限动画并给出收尾方案。
  4. 滚动与尺寸:分别实现容器滚动和页面滚动距离读取;窗口 resize 防抖输出视口宽高,要求避免高频卡顿。

原视频对照 ​

#视频时长
1图片加载失败事件05:43
2过渡事件09:38
3动画事件06:18
4滚动事件05:09
5视口尺寸变化事件02:13

小结 ​

  1. 事件监听顺序比事件名更关键,图片 error 和动态资源尤其要先监听后触发。
  2. 过渡/动画事件天然可能多次触发,必须按 target、propertyName、轮次等条件过滤。
  3. 有延迟时区分 transitionrun 与 transitionstart;收尾优先用 transitionend,无限动画不能依赖 animationend。
  4. 滚动距离的来源必须与滚动主体一致,页面滚动和容器滚动不要混用。
  5. resize 只监听 window,高频事件必须防抖/节流,避免回调内做重布局。
  6. 占位兜底要有状态标记,防止备用资源失败后递归触发。
  7. 工程中应封装通用监听与清理逻辑,避免业务层散落重复事件处理。