前言
前几天看到某位大佬的一篇名为《你们都被VUE惯坏了》的文章,没怎么细读,大概意思就是拿轮播图举例,来告诫各位开发者脱离框架外还是要掌握JS的基础功能,要多了解各种库实现的原理,虽然那篇文章有说到轮播图的一些实现思想,但是要实现一个库,要注意的细节还是很有很多,所以决定写下这一篇文章,重新回味一遍JS版的轮播图。
涉及到的知识点
我所写的轮播图90%都是由JS来实现,HTML和CSS只是用来构建组件容器和样式,其中会用到ES6的类、Proxy以及requestAnimationFrame,在这篇文章中也会随带着说一下这方面的知识点,但是不会细讲。
实现思路
无缝轮播图最灵魂的一个点应该就是无缝轮播了,所谓无缝轮播,就是你点击那一张,效果上都是从下一张or上一张平滑的划过去,在来一个合适的速度,给人的感觉就很舒畅。接下来一张图能完全表现出其思路。
就如图中的步骤:
- 点击右侧按钮,在当前轮播图父容器中将下一张图片加进去。
- 改变现在两张图的位置,也就是left,使第二张图的位置滑到第一张图,要有过渡效果。
- 销毁第一张图,使第二张图变成第一张图。 不管是左滑还是右滑,其思路就是在当前图的前边/后边在塞进去一张图,完全塞好以后,在进行移动。上图中我没有在底部加上当前图片的在第几个的小圆点,若是从第一张直接跳到第五张也是这个效果。
代码实现
基础的HTML以及CSS
<!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>
* {
padding: 0;
margin: 0;
box-sizing: border-box;
}
.carousel {
position: relative;
margin: auto;
overflow: hidden;
}
.imgCon {
position: absolute;
left: 0;
right: 0;
}
</style>
</head>
<body>
<div class="carousel">
<!-- 轮播图的最外层的容器 -->
<div class="imgCon"></div>
<!-- 轮播图图片的容器-->
</div>
<script>
/*
JS代码在这里
*/
</script>
</body>
</html>
carousel为轮播图的总容器,采用相对定位。imgCon为轮播图图片的容器,使用绝对定位,因为我们在切换图片时是在这个容器中增加DOM。
下面开始写JS代码
设定容器参数
<script>
/*
JS代码在这里
*/
const config = {
img: [
"img/a.jpeg",
"img/b.jpeg",
"img/c.jpeg",
"img/d.jpeg",
"img/e.jpeg",
],
leftClick: "img/left.png",
rightClick: "img/right.png",
autoPlay:true,
autoPlayTime:300,
time: 1500,
};
class Banner {
constructor(config) {
if (typeof config !== "object" || config === null) {
throw Error("config must be a object");
}
const { img, leftClick, rightClick, autoPlay, time, autoPlayTime } = config;
this.img = img;
this.leftClick = leftClick;
this.rightClick = rightClick;
this.autoPlay = autoPlay;
this.time = time;
this.getSize();
this.createCacheImage();
}
getSize() {
this.WIDTH = document.body.clientWidth;
this.HEIGHT = this.WIDTH / 3;
}
createCacheImage() {
this.bannerCache = this.img.map((src) => {
const img = new Image();
img.src = src;
const style = {
width: this.WIDTH + "px",
height: this.HEIGHT + "px",
};
Banner.setStyle(img, style);
return img;
});
this.btnCache = [this.leftClick, this.rightClick].map((src,index) => {
const img = new Image();
img.src = src;
const className = index === 0 ? "left" : "right";
img.type = className;
img.setAttribute('class',`${className}_btn`)
return img;
});
}
}
new Banner(config);
</script>
- config为轮播图需要的参数,分别为轮播图URL,两侧按钮(也就是上一张/下一张)URL,是否为自动轮播以及切换图片所需要的时间。
- 我们把所有的配置参数都存到this中,随用随取。
getSize为我设定的轮播图的尺寸,选这个尺寸的原因是因为我自己的图片比较大,全屏看着比较舒服,高度也是由图片来决定的,将宽高都存到this中,以后会用到。createCacheImage将轮播图片和按钮图片存到一个Map中,当我们切换下一张时可以直接偶从Map中取出来,而不用重新在创建图片DOM了,同时设置每张轮播图的样式。在btnCache循环中,我们把两个按钮通过设定type用以区分是左边还是右边,为了在点击事件中能够方便的区分;同时设为不同的类名,是为了样式能够定位到轮播图两侧(在这里css代码就不贴了。
class Banner {
static addEvent = (type, callback, target = window) => {
return target.addEventListener(type, callback);
};
static setStyle = (element, style = {}) => {
Object.assign(element.style, style);
return element;
};
constructor(config) {
if (typeof config !== "object" || config === null) {
throw Error("config must be a object");
}
const { img, leftClick, rightClick, autoPlay, time } = config;
this.img = img;
this.leftClick = leftClick;
this.rightClick = rightClick;
this.autoPlay = autoPlay;
+ Banner.addEvent("resize", this.updateSize.bind(this));
this.time = time;
this.getSize();
this.createCacheImage();
+ this.setBannerSize();
}
//以下为新增、之前的函数方法不变
updateSize() {
this.WIDTH = document.body.clientWidth;
this.HEIGHT = this.WIDTH / 3;
}
setBannerSize() {
if(!this.carousel){
this.carousel = document.querySelector(".carousel");
}
const style = {
width: this.WIDTH + 'px',
height: this.HEIGHT + 'px'
}
Banner.setStyle(this.carousel, style);
}
}
setStyle和addEvent是封装的公共的一个方法,分别是添加DOM样式和添加DOM事件,因为这两个方法通过函数传参就可以完成其功能,不需要获取当前this的任何信息,所以用static关键字写成了静态方法。- 为
window添加resize事件,是为了兼容窗口大小随时变化的可能,宽度变化时更新width和height。 setBannerSize将carousel也就是轮播图的总容器设置宽高。
添加图片
class Banner {
// 静态方法省略
constructor(config) {
// 省略之前的方法
+ this.appendImage();
+ this.appendBtn();
}
//以下为新增、之前的函数方法不变
appendImage(currentImage = 0) {
if (typeof currentImage !== "number") {
return;
}
if (!this.imgCon) {
this.imgCon = document.querySelector(".imgCon");
const style = {
width:this.WIDTH + 'px',
height: this.HEIGHT + 'PX',
}
this.setStyle(this.imgCon, style);
this.carousel.appendChild(this.imgCon);
}
return this.imgCon.appendChild(this.bannerCache[currentImage]);
}
appendBtn() {
this.btnCache.forEach((btn) => {
Banner.addEvent("click", this.handleBtnClick.bind(this), btn);
this.carousel.appendChild(btn);
});
}
handleBtnClick(e){
// 先省略
}
}
appendImage将当前第N个图片添加到轮播图的父容器中,这里我们也是在HTML中写好了DOM,取出来直接存到this中,方便下次操作。这里我们参数currentImage表示要添加的第几个图片,因为之前我们已经将所有轮播图片缓存到了bannerCache中,所以可以通过索引从bannerCache中直接取出来进行添加,也是方便接下来我们若是点击轮播图下方的小圆点,可以通过其索引直接进行添加。appendBtn中省略handleBtnClick具体细节。 到了这一步我们可以先看一下我们的效果好,基本上就是这个样式,但是这样有一个问题,就是我改变窗口大小的时候,轮播图容器的尺寸并没有发生相应的改变。
原因是我们虽然监听了window的
resize事件,并且动态的修改了this.WIDTH和this.HEIGHT的值,但是DOM的尺寸并不会因为你手动修改了其值而动态的改变。(突然怀念起了框架的好处是怎么回事?) 接下来我们要监听一下WIDTH和HEIGHT的变化。
使用Proxy监听
class Banner {
// 以上省略
static bindProxy = (object, callback, targetChangeCallback) => {
if (typeof object !== "object" || object === null) return;
const proxy = new Proxy(object, callback(targetChangeCallback));
return proxy;
};
constructor(){
// ...以上省略
Banner.addEvent("resize", this.getSize.bind(this)); // 将resize回调函数设置getSize
this.sizeInfo = {};
this.sizeInfo = Banner.bindProxy(
this.sizeInfo,
this.sizeInfoChange,
this.sizeInfoChangeCallback.bind(this)
);
this.getSize() //将this.getSize写到Proxy后边
}
getSize() {
this.WIDTH = document.body.clientWidth;
this.HEIGHT = this.WIDTH / 3;
this.sizeInfo.width = this.WIDTH;
this.sizeInfo.height = this.HEIGHT;
}
/*此函数干掉,已经没有意义了
updateSize() {
this.WIDTH = document.body.clientWidth;
this.HEIGHT = this.WIDTH / 3;
}
)
*/
createCacheImage() {
this.bannerCache = this.img.map((src) => {
const img = new Image();
img.src = src;
/*
const style = {
width: this.WIDTH + "px",
height: this.HEIGHT + "px",
};
*/
Banner.setStyle(img, this.sizeInfo);
return img;
});
this.btnCache = [this.leftClick, this.rightClick].map((src) => {
const img = new Image();
img.src = src;
return img;
});
}
setBannerSize() {
if(!this.carousel){
this.carousel = document.querySelector(".carousel");
}
/*
const style = {
width: this.WIDTH + "px",
height: this.HEIGHT + "px",
};
*/
Banner.setStyle(this.carousel, this.sizeInfo);
}
appendImage(currentImage = 0) {
if (typeof currentImage !== "number") {
return;
}
if (!this.imgCon) {
this.imgCon = document.querySelector(".imgCon");
/*
Banner.setStyle(this.imgCon, {
width: this.WIDTH + "px",
height: this.HEIGHT + "px",
});
*/
Banner.setStyle(this.imgCon, this.sizeInfo);
this.carousel.appendChild(this.imgCon);
}
return this.imgCon.appendChild(this.bannerCache[currentImage]);
}
sizeInfoChange(callback) {
if (callback && typeof callback !== "function") {
throw Error("callback must be a function!");
}
return {
get: (target, key) => {
return target[key];
},
set: (target, key, value) => {
if (!["width", "height"].includes(key)) {
throw Error("must be width or height");
}
if (typeof value !== "number") {
throw Error("must be type of number");
}
const change = Reflect.set(target, key, value + "px");
if (key === "height") {
callback && callback();
}
return change;
},
};
}
handleBtnClick = () => {
// 先省略
}
sizeInfoChangeCallback() {
this.setBannerSize();
this.bannerCache.forEach((img) => {
Banner.setStyle(img, this.sizeInfo);
});
}
}
- 简单科普一下
Proxy的用法:
Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。Proxy 这个词的原意是代理,用在这里表示由它来“代理”某些操作,可以译为“代理器”。
var obj = new Proxy({}, {
get: function (target, propKey, receiver) {
console.log(`getting ${propKey}!`);
return Reflect.get(target, propKey, receiver);
},
set: function (target, propKey, value, receiver) {
console.log(`setting ${propKey}!`);
return Reflect.set(target, propKey, value, receiver);
}
});
obj.count = 1
// setting count!
++obj.count
// getting count!
// setting count!
// 2
和Object.defineProperty有异曲同工之妙,不过Proxy更优秀一些,在这里我不做详细阐述,掘金搜索Proxy有几篇不错的文章详细的讲述了Proxy的原理以及使用。
简单的说在这里我们就是在resize事件触发时,修改sizeInfo中width和height属性值,从而使回调函数sizeInfoChangeCallback触发,改变轮播图最外层容器的尺寸以及每张轮播图片的尺寸。
2. 将this.getSize移到Banner.bindProxy后边,是因为先对sizeInfo进行监听,然后通过getSize获取尺寸、修改尺寸,从而达到初次加载的时候设定容器以及图片的宽高。若是getSize在Banner.bindProxy之前,在监听sizeInfo之前尺寸就已经有了,而我们把所有用到width和height的地方全部改成了sizeInfo,那么初次加载就拿不到width和height的信息了。
现在容器和图片的尺寸会随着屏幕的变化而变化了
无缝轮播
接下来是轮播图的核心功能:无缝轮播。当我们点击左右按钮时,会无缝切换到上一张/下一张。
当点击左按钮时,在当前图片的前边增加其上一张图片,成功后轮播图的容器向右滑动。
点击右按钮则反过来。
新增节点
接下来我们完成之前空置的按钮事件。
class Banner{
constructor(config){
// ...以上所有代码不变
this.currentImage = {
index: 0, //当前显示的图片
};
this.currentImage = Banner.bindProxy(
this.currentImage,
this.currentImageChange,
this.sizeInfoChangeCallback.bind(this)
);
this.isPlaying = false; //是否正在移动
}
handleBtnClick(){
if (this.isPlaying) return;
e.preventDefault();
e.stopPropagation();
const { type } = e.target;
let currentImage;
this.direction = type;
if (type === "left") {
currentImage = this.currentImage.index - 1;
if (currentImage < 0) {
currentImage = this.bannerCache.length - 1;
}
return (this.currentImage.index = currentImage);
}
if (type === "right") {
currentImage = this.currentImage.index + 1;
if (currentImage >= this.bannerCache.length) {
currentImage = 0;
}
return (this.currentImage.index = currentImage);
}
}
currentImageChange(callback){
if (callback && typeof callback !== "function") {
throw Error("callback must be a function!");
}
return {
get: (target, key) => {
return target[key];
},
set: (target, key, value) => {
if (key !== "index") {
throw Error("you can set `index` property at currentImage");
}
if (typeof value !== "number") {
throw Error("must be type of number");
}
const change = Reflect.set(target, key, value);
callback && callback();
return change;
},
};
}
currentImageChangeCallback() {
const type = this.direction;
this.imgCon.style.width = this.WIDTH * 2 + "px";
if (type === "left") {
this.imgCon.insertBefore(
this.bannerCache[this.currentImage.index],
this.imgCon.firstElementChild
);
this.imgCon.style.left = -this.WIDTH + "px";
} else {
this.appendImage(this.currentImage.index);
}
this.isPlaying = true;
}
}
- 先看
this.currentImage,因为我们切换图片是要切换当前index,也就是切换bannerCache的索引,所以当index改变时我们进行操作,跟监听宽高变化时一样,使用Proxy进行监听当前图片索引的变化。 this.isPlaying表明当前是否是在轮播切换中,若是则阻止一切操作,因为若切换图片时连续点击按钮,频繁操作会影响交互。- 看
handleBtnClick中的代码,我们之前为左右按钮增加了type属性,此时就用到了,若是点击的左侧按钮,先将此时点击的方向存到this.direction中,接下来会用到。点击左侧按钮代表切换上一张图片,假设当前索引是2,也就是显示的第3张图片,那么就应该将第2张图片添加到当前图片前边,进行右滑,点击右侧按钮相反。当前这里需要注意边界条件,就是第0张和最后一张。 - 点击哪个按钮应该切换到第几张图我们拿到这个结果后,赋值给
currentImage的index属性,Proxy中回调函数currentImageChangeCallback触发。 - 因为不管是左滑还是右滑,都会在当前轮播图容器中增加一张图,所以需要先设置当前容器width为原来的2倍,然后在根据之前存的
direction来判断是向前增加图片还是向后增加图片。 - 有一个样式问题需要注意,若是点击左侧按钮,需要向当前图片节点之前增加一个节点,若是增加成功,新增的节点会直接将当前节点挤到后边去,取而代之显示的就是新增的节点了,我们要手动处理left的距离,然后通过动画将上述行为展现出来。
我们分别点击一下左右侧的按钮:
点击左侧按钮:在当前节点之前增加
点击右侧按钮:在当前节点之后增加
requestAnimationFrame动画过渡
class Banner{
constructor(config){
//...省略以上代码
this.FPS = 60;
this.getFPS();
this.computeSpeed()
this.animation();
}
getFPS() {
let now = Date.now();
let lastNow = Date.now();
let count = 0;
let countTotal = 0;
const countArr = [];
let computeFrame = null;
const compute = () => {
now = Date.now();
count++;
computeFrame = requestAnimationFrame(compute);
if (now - lastNow >= 1000) {
lastNow = Date.now();
const length = countArr.push(count);
countTotal = countTotal + count;
count = 0;
if (length === 5) {
this.FPS = Math.round(countTotal / 5);
cancelAnimationFrame(computeFrame);
}
}
};
compute();
}
computeSpeed(){
const rCount = 1000 / this.FPS;
this.speed = this.WIDTH / (this.time / rCount);
}
animation() {
this.callback = requestAnimationFrame(this.animation.bind(this));
this.carouselPlaying();
}
carouselPlaying() {
if (!this.isPlaying) return;
const direction = this.direction;
const imgCon = this.imgCon;
if (direction === "left") {
imgCon.style.left = imgCon.offsetLeft + this.speed + "px";
if (imgCon.offsetLeft >= 0) {
this.isPlaying = false;
imgCon.lastElementChild.remove();
imgCon.style.left = "0px";
}
} else if (direction === "right") {
imgCon.style.left = imgCon.offsetLeft - speed + "px";
if (imgCon.offsetLeft <= -this.WIDTH) {
this.isPlaying = false;
imgCon.firstElementChild.remove();
imgCon.style.left = "0px";
}
}
}
}
在我们新增了一个节点后,需要让当前轮播图的容器进行左/右移动,使新增的节点显现出来,这里我们使用requestAnimationFrameAPI。它在MDN的解释:window.requestAnimationFrame() 告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行。
也就是浏览器每次重绘来调用其回调函数,重绘频率为屏幕的刷新率,简称FPS,也就是每秒屏幕刷新的次数。
一般我们的屏幕刷新频率为60HZ,也就是每秒刷新60次,平均16.7毫秒刷新一次,但是我们不知道轮播图真正跑在多HZ的屏幕上,所以这里需要计算一下。
我们在初始的config参数中传了time = 1500ms,所以我们要在1500ms内完成轮播的动作,那么计算:
- 假设屏幕刷新率为N,刷新一次需要的毫秒数 = 1000/每秒刷新的次数(N)
- 1500毫秒完成动作,需要刷新的次数 = 1500 / 刷新一次需要的毫秒数
- 所以 每一次刷新需要移动的距离 = 总共需要移动的总距离 / 需要刷新的次数
computeSpeed在设定屏幕刷新率为60HZ情况下计算每一次刷新的速度,而与此同时屏幕真正的FPS也在计算中,通过getFPS函数中compute函数每一秒运行的次数,取5次的平均值就可以得出准确的FPS,得出FPS后在从新计算speed。
在初始化的时候就运行animation函数,在内部使用requestAnimationFrame回调本身和carouselPlaying,若是点击左/右按钮之一,this.isPlaying值设为true,carouselPlaying中this.isPlaying一旦为true,后续的代码就能在每一帧中被运行,通过判断direction是进行向左还是向右移动,进行移动,carouselPlaying每16.7ms运行一次,直到将下个要展示的图片拉倒当前位置,然后销毁上一张,整个轮播过程就完成了。 注意:移动的过程我们用的是offsetLeft,也就是轮播图父容器边缘距离总容器的距离,计算出需要移动的距离,然后改变轮播图父容器的left值。下图可以清晰的表达出offsetLeft的意义我们来看一下现在的轮播效果
圆点跳播
有没有觉得这个轮播图缺了一点东西?嗯?没错,就是轮播图下边的小圆点,没有圆点进度,只能一个一个的播放,若是从第一张直接跳第三张呢?接下里我们开始增加小圆点。
html里增加一个圆点的父容器,然后设定好其样式
<style>
* {
padding: 0;
margin: 0;
box-sizing: border-box;
}
.carousel {
position: relative;
margin: auto;
overflow: hidden;
margin: 30px auto;
}
.imgCon {
position: absolute;
left: 0;
right: 0;
}
.left,
.right {
position: absolute;
top: 50%;
transform: translateY(-50%);
}
.left {
left: 10px;
}
.right {
right: 10px;
}
/* 圆点父容器 */
.processDot {
position: absolute;
bottom: 20px;
width: 200px;
left: 50%;
transform: translateX(-50%);
display: flex;
justify-content: space-around;
z-index: 2;
}
/* 每个小圆点的样式 */
.process_dot {
list-style: none;
width: 30px;
height: 30px;
border-radius: 50%;
border: 1px solid red;
}
/* 当前轮播图片索引和圆点索引一致时 */
.process_dot[active="true"] {
background-color: red;
}
</style>
<div class="carousel">
<!-- 轮播图的最外层的容器 -->
<div class="imgCon"></div>
<!-- 轮播图图片的容器-->
<ul class="processDot"></ul>
</div>
class Banner{
constructor(){
// ...省略以上所有
this.appendDot();
}
handleDotClick(index, e) {
e.preventDefault();
e.stopPropagation();
if (this.isPlaying) return;
if (index > this.currentImage.index) {
this.direction = "right";
}
if (index < this.currentImage.index) {
this.direction = "left";
}
this.currentImage.index = index;
}
appendDot() {
this.processDot = document.querySelector(".processDot");
const fragment = document.createDocumentFragment();
this.dotCache = this.bannerCache.map((img, index) => {
const dot = document.createElement("li");
dot.setAttribute("class", "process_dot");
if (index === this.currentImage.index) {
dot.setAttribute("active", true);
}
Banner.addEvent(
"click",
this.handleDotClick.bind(this, index),
dot
);
fragment.appendChild(dot);
return dot;
});
this.processDot.appendChild(fragment);
}
currentImageChangeCallback() {
const type = this.direction;
this.imgCon.style.width = this.WIDTH * 2 + "px";
if (type === "left") {
this.imgCon.insertBefore(
this.bannerCache[this.currentImage.index],
this.imgCon.firstElementChild
);
this.imgCon.style.left = -this.WIDTH + "px";
} else {
this.appendImage(this.currentImage.index);
}
const index = this.currentImage.index;
+ this.dotCache.forEach((dot, dotIndex) => {
+ if (index === dotIndex) {
+ return dot.setAttribute("active", true);
+ }
+ dot.setAttribute("active", false);
+ });
this.isPlaying = true;
}
}
- 根据轮播图数量创建小圆点,把当前正在轮播的图片索引对应的小圆点设置
active属性,并为每一个小圆点增加点击事件 - 点击小圆点时,判断每个小圆点的索引与当前轮播图的索引关系,找出应该轮播的方向,并将索引赋值给要轮播到的图片。
- 在
currentImage.index回调函数中增加小圆点索引与当前轮播索引的判断,若相等,则active为true,否则为false。 来看一下效果:目前为止我们的轮播图就已经完成了85%,那么还差什么呢?没错,自动轮播!
当页面停留&鼠标在轮播图之外我们进行自动轮播,为什么呢?因为鼠标一旦hover在轮播图上面,大概率用户要进行某些轮播操作,比如点击事件,查看大图事件...。
自动轮播
class Banner{
static removeEvent = (type, callback, target = window) => {
return target.removeEventListener(type, callback);
};
constructor(){
//... 省略
+ this.autoPlayTime = autoPlayTime/this.rCount;
+ this.cuteTime = this.autoPlayTime;
+ this.autoPlayEvent();
+ this.addMouseEvent();
}
autoPlayEvent() {
if (this.autoPlay) {
this.autoPlaying = true;
}
}
addMouseEvent() {
if (this.autoPlay) {
Banner.addEvent(
"mouseenter",
this.handleMouse.bind(this),
this.carousel
);
Banner.addEvent(
"mouseleave",
this.handleMouse.bind(this),
this.carousel
);
}
}
handleMouse(e) {
if (e.type === "mouseenter") {
this.autoPlaying = false;
}
if (e.type === "mouseleave") {
this.autoPlaying = true;
}
}
computeSpeed() {
//const rCount = 1000 / this.FPS;
this.rCount = 1000 / this.FPS;
this.speed = this.WIDTH / (this.time / this.rCount);
}
animation() {
this.callback = requestAnimationFrame(this.animation.bind(this));
this.carouselPlaying();
+ this.handleAutoPlay();
}
handleAutoPlay() {
if (!this.autoPlaying) return;
this.cuteTime--;
if (this.cuteTime > 0) return;
this.cuteTime = this.autoPlayTime;
var evt = new MouseEvent("click");
this.btnCache[1].dispatchEvent(evt);
}
}
- 先看新增的
autoPlayEvent函数,判断autoPlay是都需要自动轮播,若是,开启自动轮播的开关autoPlaying。 animation函数中新增了一个回调函数handleAutoPlay,就是让图片进行自动轮播使用,内部逻辑是判断自动轮播开关autoPlaying是否已经开启,若开启,按照传入的时间,隔那个时间轮播一次- 因为
autoPlayTime是用户想要隔多少秒需要进行一次轮播,而我们的关键帧运动是通过计算得出的,若是60PFS就是每16.7ms执行一次handleAutoPlay,而this.cuteTime每次只减1,所以需要计算一下达到用户想要的那个轮播时间。 - 自动轮播我们使用的是自定义事件,因为左右两侧的按钮已经有了点击事件来进行轮播,所以我们只要通过触发右侧按钮点击事件就可以进行轮播了。
- 为轮播图整体添加鼠标进入和移除事件,因为鼠标一旦hover在轮播图上,那么用户的目的很大概率是要进行点击事件,这时候就应该停止轮播了。
最后,这个轮播图就已经完成了,我们来看一下最终的效果
ohshit!gif图片太大了掘金装不下。。
代码地址先献上
总结
总结一下轮播图中的各种细节:
- 使用
Proxy代替传统的回调函数,通过监听数据的改变来执行事件,可以使代码更好的解耦。 requestAnimationFrame代替传统的setTimeout,理论上可以使动画更加丝滑,并且节省性能。- 缓存图片值,每次轮播不要重新再去请求图片,侧面也节省了性能。
- 考虑边界条件,比如轮播过程中需要禁止轮播事件再次执行,鼠标hover禁止自动轮播。
在写作过程中,笔者还想到在创建图片的时候其实应该用
Promise,轮播图片全部加载成功整个轮播图才可以创建,因为图片加载时异步的,若是第一张图片加载时间过长那么就会发生JS代码错误,其中某个图片加载失败也要考虑相应的处理办法~。