这轮播图居然还能涉及到生成器函数、浏览器的渲染原理以及js事件循环???

302 阅读4分钟

最近看见B站上有个在安利动画库的视频,其中讲到使用生成器函数模拟动画中这个概念。
为此我大受启发!我突然就参悟到,这个生成器函数在前端领域的一大用途了!
那就是依靠模拟这个概念来完成各种动画效果!
我们来开辟一下思路!
这是一个生成器函数:

const generator = ((function* animationGenerator() {
  yield console.log("1")
  yield console.log("2")
  yield console.log("3")
})());

我们通过生成器函数返回的generator,来控制每一次yield的触发

image.png

每一次generator.next()函数的触发就会让函数执行到,生成器遇见下一个yield关键字的地方,然后在yield关键字的地方暂停

image.png
image.png
image.png
也就是说:每一个yield关键字都会执行上一个yield关键字之间的函数!

那么,我们升级一下~
我们用这种思路来写一个轮播图来玩!


普通轮播图+setInterval函数启动

先创建一个index.html文件,稍微把一个轮播图的样式和需要的dom结构给搞出来~

<!DOCTYPE html>
<html lang="en">

  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
      #banner {
        width: 500px;
        height: 300px;
        margin: auto;
        overflow: hidden;
        border: 3px solid rgb(0, 162, 255);
      }

      #imgContent {
        display: flex;
        transform: translateX(0px)
      }

      img {
        width: 500px;
        height: 300px;
      }
    </style>
  </head>
  <body>
    <div id="banner">
      <div id="imgContent" style="transform: translateX(0px);">
        <img id="img-1" src="./image/图1.jpg">
        <img id="img-2" src="./image/图2.jpg">
        <img id="img-3" src="./image/图3.jpg">
        <img id="img-4" src="./image/图4.jpg">
        <!-- 可拓展图片 -->
      </div>
    </div>
    <script>
      // todo
    </script>
  </body>
</html>

代码如上,不多讲解了,长这个样子:
image.png
内部的图片就是小米官网上的轮播图(我没打广告啊,我纯爬的素材)
然后就是获取各种元素

<script>
    const imgList = document.querySelectorAll('img');
    const imgContent = document.getElementById('imgContent');
    const imgWidth = 500;
</script>

轮播图的实现一定是基于样式上的修改,一般来说是采用js修改translateX来实现轮播图的切换,设置transition-duration来实现过渡效果
我们先来写一个根据图片索引展示图片的函数:
图片索引自然就是根据imgContent.length获取内部的img标签的数量

<script>
  const imgList = document.querySelectorAll('img');
  const imgContent = document.getElementById('imgContent');
  const imgWidth = 500;

  // 根据图片索引展示图片,isAnimation设置是否开启动画效果
  function showImageByIndex(imgIndex = 0, isAnimation = true) {
    console.log("当前图片是", imgIndex);
    const transformX = imgIndex * imgWidth;
    imgContent.style.transitionDuration = isAnimation ? '0.5s' : '0s';
    imgContent.style.transform = `translateX(${-transformX}px)`;
  }
</script>

现在我们完成根据图片索引展示图片的函数了,现在便是重头戏!
现在让我们根据生成器函数完成每一个图片的切换!

// 使用生成器函数完成轮播图
const bannerAnimation = ((function* bannerAnimationGenerator() {
  let imgIndex = 0;
  while (true) {
    yield showImageByIndex(imgIndex, true)
    imgIndex++ //索引+1,移动到下一张图片
    imgIndex = imgIndex % imgList.length; // 当索引值等于图片数时,取模运算会返回除法运算的余数自动变成“0”
  }
})());

// 启动动画
window.onload = () => {
  console.log("全局图片资源挂载完毕");
  setInterval(() => {
    bannerAnimation.next();
  }, 2 * 1000); // 实现2s切换一张图片
}

现在我们来看看长什么样!

是不是有一种精妙的感觉!感觉突然灵感迸发!
现在我们的轮播图只是一个普普通通的轮播图,甚至无法做到无缝轮播图!现在来优化一下!


无缝轮播图+setInterval函数启动

无缝轮播图大多是让第一张图片与最后一张图片为同一张图片,来实现欺骗用户眼睛的效果,我们来修改一下dom结构

<body>
  <div id="banner">
    <div id="imgContent" style="transform: translateX(0px);">
      <img id="img-1" src="./image/图1.jpg">
      <img id="img-2" src="./image/图2.jpg">
      <img id="img-3" src="./image/图3.jpg">
      <img id="img-4" src="./image/图4.jpg">
      <!-- 可再次向下拓展图片 -->
      <img id="img-1" src="./image/图1.jpg">
    </div>
  </div>
</body>

在第9行的位置,我添加了第一张图片
在其余代码没有变化的情况下,现在我们再来修改一下我们的生成器函数,实现无缝轮播图的效果~

const bannerAnimation = ((function* bannerAnimationGenerator() {
  let imgIndex = 0;
  while (true) {
    console.time('move')
    yield showImageByIndex(imgIndex, true)
    imgIndex++
    imgIndex = imgIndex % imgList.length;
    if (imgIndex === 0) {
      showImageByIndex(0, false);
      imgIndex = 1
    }
    console.timeEnd('move')
  }
})());

我们添加了一个if语句来适应,当轮播图到最后一张图时,直接跳转到第一张图去的代码逻辑

注意:0才是第一张图,因为我们是根据imgContent.length来获取图片数量的

现在我们来看看效果~

怎么回事,怎么出现最后一张图倒转到第二张图的现象了!
这种问题的本质原因是:同步代码中多次修改dom样式实际上会被合并成一次dom操作!
换句话说,就是第9句执行的showImageByIndex(0, false)函数与马上就执行的第5句yield showImageByIndex(imgIndex, true)函数,出现了dom操作任务合并的现象!
不对啊,不应该是先执行yield showImageByIndex(imgIndex, true)函数,再执行showImageByIndex(0, false)函数吗?其实不对!
还记得我在最上面画的图吗?

image.png

看见没有,执行的逻辑是:上一次yield关键执行后就暂停了,必须等bannerAnimation.next()函数执行后执行下一波函数!
换到这个轮播图函数也一样,即上一张图执行完毕了,才使用showImageByIndex()函数切换到下一张图。
而特殊的地方就在最后一张图妄图切换到第一张图时,出现了dom操作任务合并的现象!
我们把showImageByIndex()函数摊开来看:

const bannerAnimation = ((function* bannerAnimationGenerator() {
  let imgIndex = 0;
  while (true) {
    console.time('move')
    yield ((() => {
      const transformX = imgIndex * imgWidth;
      imgContent.style.transitionDuration = '0.5s';
      imgContent.style.transform = `translateX(${-transformX}px)`;
    })())
    imgIndex++
    imgIndex = imgIndex % imgList.length;
    if (imgIndex === 0) {
      const transformX = imgIndex * imgWidth;
      imgContent.style.transitionDuration = '0s';
      imgContent.style.transform = `translateX(${-transformX}px)`;
      imgIndex = 1
    }
    console.timeEnd('move')
  }
})());
// 根据图片索引展示图片,isAnimation设置是否开启动画效果
function showImageByIndex(imgIndex = 0, isAnimation = true) {
  console.log("当前图片是", imgIndex);
  const transformX = imgIndex * imgWidth;
  imgContent.style.transitionDuration = isAnimation ? '0.5s' : '0s';
  imgContent.style.transform = `translateX(${-transformX}px)`;
}

发现没有,这个对imgContent的样式操作居然是一段同步代码!
渲染任务自然会在同步代码全部执行完成之后,再在微任务全部执行之后再执行我们的渲染任务,而宏任务又是在渲染任务完成之后触发~
执行流程就是:
同步代码-->微任务-->渲染任务-->宏任务
那么,我们需要做的就是让showImageByIndex(0, false)函数的执行,在showImageByIndex(imgIndex, true)函数之前执行。
那么我们只需要让两个函数不再同步即可,但却保留执行的先后顺序,这也很简单:
yield关键字后添加setTimeout函数,把这个渲染任务放到宏任务中去即可

const bannerAnimation = ((function* bannerAnimationGenerator() {
  let imgIndex = 0;
  while (true) {
    console.time('move')
    yield setTimeout(() => {
      showImageByIndex(imgIndex, true)
    }, 0);
    imgIndex++
    imgIndex = imgIndex % imgList.length;
    if (imgIndex === 0) {
      showImageByIndex(0, false);
      imgIndex = 1
    }
    console.timeEnd('move')
  }
})());

此时大功告成?
可是谁家动画是使用setInterval函数来作为动画的定时器的???
我依稀记得,有个名称为requestAnimationFrame的函数能让其中的回调函数,执行的时间出现在浏览器回流之前
现在我们再使用requestAnimationFrame函数再来优化一下~

注意:requestAnimationFrame函数太长了,我们现在叫他raf函数


无缝轮播图+raf函数启动

现在的启动函数长这个样子了

// 启动动画
window.onload = () => {
  console.log("全局图片资源挂载完毕");
  let oldTime = new Date().getDate();
  function tick(newTime) {
    if (newTime - oldTime > 2000) {
      oldTime = newTime;
      bannerAnimation.next();
    }
    requestAnimationFrame(tick)
  }
  requestAnimationFrame(tick)
}

这样有什么优点呢?根据mdn文档所述:

  1. 回调函数的调用频率通常与显示器的刷新率相匹配
  2. 关闭浏览器会睡眠该函数中的回调函数

现在最后的代码与效果变成了这样: 代码变成了这样

<!DOCTYPE html>
<html lang="en">

  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
      #banner {
        width: 500px;
        height: 300px;
        margin: auto;
        overflow: hidden;
        border: 3px solid rgb(0, 162, 255);
      }

      #imgContent {
        display: flex;
        transform: translateX(0px)
      }

      img {
        width: 500px;
        height: 300px;
      }
    </style>
  </head>

  <body>
    <div id="banner">
      <div id="imgContent" style="transform: translateX(0px);">
        <img id="img-1" src="./image/图1.jpg">
        <img id="img-2" src="./image/图2.jpg">
        <img id="img-3" src="./image/图3.jpg">
        <img id="img-4" src="./image/图4.jpg">
        <!-- 可再次向下拓展图片 -->
        <img id="img-1" src="./image/图1.jpg">
      </div>
    </div>
    <script>
      /**
        * 无缝轮播图-生成函数+Raf函数为启动动画版本
        */
      const imgList = document.querySelectorAll('img');
      const imgContent = document.getElementById('imgContent');
      const imgWidth = 500;

      // 使用生成器函数完成轮播图
      const bannerAnimation = ((function* bannerAnimationGenerator() {
        let imgIndex = 0;
        while (true) {
          console.time('move')
          yield setTimeout(() => {
            showImageByIndex(imgIndex, true)
          }, 0);
          imgIndex++
          imgIndex = imgIndex % imgList.length;
          if (imgIndex === 0) {
            showImageByIndex(0, false);
            imgIndex = 1
          }
          console.timeEnd('move')
        }
      })());

      // 启动动画
      window.onload = () => {
        console.log("全局图片资源挂载完毕");
        let oldTime = new Date().getDate();
        function tick(newTime) {
          if (newTime - oldTime > 2000) {
            oldTime = newTime;
            bannerAnimation.next();
          }
          requestAnimationFrame(tick)
        }
        requestAnimationFrame(tick)
      }

      // 根据图片索引展示图片,包括设置动画效果
      function showImageByIndex(imgIndex = 0, isAnimation = true) {
        console.log("当前图片是", imgIndex);
        const transformX = imgIndex * imgWidth;
        imgContent.style.transitionDuration = isAnimation ? '0.5s' : '0s';
        imgContent.style.transform = `translateX(${-transformX}px)`;
      }
    </script>
  </body>

</html>

小小的生成器与轮播图,居然还涉及到了浏览器的渲染原理与事件循环。不得不说,有点意思~