最近看见B站上有个在安利动画库的视频,其中讲到使用生成器函数模拟动画中帧这个概念。
为此我大受启发!我突然就参悟到,这个生成器函数在前端领域的一大用途了!
那就是依靠模拟帧这个概念来完成各种动画效果!
我们来开辟一下思路!
这是一个生成器函数:
const generator = ((function* animationGenerator() {
yield console.log("1")
yield console.log("2")
yield console.log("3")
})());
我们通过生成器函数返回的generator,来控制每一次yield的触发
每一次generator.next()函数的触发就会让函数执行到,生成器遇见下一个yield关键字的地方,然后在yield关键字的地方暂停!
也就是说:每一个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>
代码如上,不多讲解了,长这个样子:
内部的图片就是小米官网上的轮播图(我没打广告啊,我纯爬的素材)
然后就是获取各种元素
<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)函数吗?其实不对!
还记得我在最上面画的图吗?
看见没有,执行的逻辑是:上一次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文档所述:
- 回调函数的调用频率通常与显示器的刷新率相匹配
- 关闭浏览器会睡眠该函数中的回调函数
现在最后的代码与效果变成了这样:
代码变成了这样
<!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>
小小的生成器与轮播图,居然还涉及到了浏览器的渲染原理与事件循环。不得不说,有点意思~