一、场景描述
在很多网站的页面中都有轮播图,所以我想利用react.js和ts实现一个轮播图。自动轮播图已经在前面实现过了,如:https://blog.csdn.net/weixin_43872912/article/details/145622444?sharetype=blogdetail&sharerId=145622444&sharerefer=PC&sharesource=weixin_43872912&spm=1011.2480.3001.8118。
思维导图可以在网站:https://download.csdn.net/download/weixin_43872912/90429355?spm=1001.2014.3001.5503下载高清原图。
二、问题拆解
轮播图(如下图轮播图所示)的实现可以分为三个:
第一个是自动轮播,就是每过一定的时间自动变成下一张图;
第二个是前后按钮的轮播,就是按左右的按钮,按左边的按钮时,跳转到前一张图,按右边的按钮时,跳转到后一张图。
第三个就是按底部的按钮切换轮播图。
三、相关知识
3.1 setTimeout与setInterval的用法与详解
setTimeout是指过了多久之后执行内部代码,一次性的;setInterval是每过多久就要执行一次代码。setTimeout里面开启计时器的话,就需要先过setTimeout的时间,然后过setInterval的一次时间才会运行第一次。
var time = 0;
setInterval(() => {
time += 1;
console.log("当前时间为" + time + "秒");
}, 1000);
let count = 0;
//test setTimeout & setInterval
var testTimeFunction = function () {
setTimeout(() => {
setInterval(() => {
count += 1;
console.log("count输出了" + count + "次");
}, 3000);
}, 3000);
};
testTimeFunction();
运行结果如下图所示:
3.2 react中的ref,利用ref获取dom元素,并对dom元素的class进行操作
typescript">import React, { useState, useRef } from "react”;
const divRef = useRef<HTMLDivElement>(null);
//tsx部分
<div ref={divRef}>
<button
onClick={() => {
console.log(divRef);
let target = divRef.current as HTMLElement; //ts里面要将其定义为htmlElement再操作
target.classList.add("preActive");
console.log(target);
}}
>打印ref
</button>
</div>
四、轮播图的按钮部分实现
4.1 上一张和下一张的按钮实现
上一张和下一张的按钮部分其实就是自动轮播部分的手动操作,这部分的内容关注一下自动轮播部分和手动轮播部分的联动就可以了。
按钮事件:
跳转的按钮事件, 可以看到其实就是设置了一下currentIndex,这是一个state,更新之后会引起组件渲染,然后就会更新dom元素的class。
typescript">//跳转到上一张图的按钮事件。
<button
className="carousel-control-prev"
type="button"
data-bs-target="#carouselExample"
onClick={() => {
//clearCrouselClass();
setCurrentIndex((pre) => {
let nowCurrentIndex =
currentIndex.currentIndex - 1 === -1
? img.length - 1
: currentIndex.currentIndex - 1;
let nowPreIndex =
nowCurrentIndex - 1 === -1
? img.length - 1
: nowCurrentIndex - 1;
let nownextIndex =
nowCurrentIndex + 1 === img.length ? 0 : nowCurrentIndex + 1;
return {
preIndex: nowPreIndex,
currentIndex: nowCurrentIndex,
nextIndex: nownextIndex,
};
});
}}
>
//跳转到下一张图的按钮事件。
<button
className="carousel-control-next"
type="button"
data-bs-target="#carouselExample"
onClick={() => {
//clearCrouselClass();
setCurrentIndex((pre) => {
let nowCurrentIndex =
currentIndex.currentIndex + 1 === img.length
? 0
: currentIndex.currentIndex + 1;
let nowPreIndex =
nowCurrentIndex - 1 === -1
? img.length - 1
: nowCurrentIndex - 1;
let nownextIndex =
nowCurrentIndex + 1 === img.length ? 0 : nowCurrentIndex + 1;
return {
preIndex: nowPreIndex,
currentIndex: nowCurrentIndex,
nextIndex: nownextIndex,
};
});
}}
>
手动换图与自动轮播图之间的不和谐在于,手动换图后自动轮播还在执行会导致换两次可能,所以手动换图的时候需要停止自动轮播,结束后开启。
换图每次都会伴随着cunrrentIndex的变动,所以在这里我们用useEffect去检测cunrrentIndex的变化,只要变化就停止计时器,然后重新开启。 — 这里我用了异步
typescript"> //开启计时器,设置preIndex是当前index的前一个index(index-1),nextIndex是index+1
const startAutoplay = async () => {
interval.current = setInterval(() => {
setCurrentIndex((preCurrentIndex) => {
return {
preIndex:
((preCurrentIndex.currentIndex + 1) % img.length) - 1 === -1
? img.length - 1
: ((preCurrentIndex.currentIndex + 1) % img.length) - 1,
currentIndex: (preCurrentIndex.currentIndex + 1) % img.length,
nextIndex:
((preCurrentIndex.currentIndex + 1) % img.length) + 1 === img.length
? 0
: ((preCurrentIndex.currentIndex + 1) % img.length) + 1,
};
});
}, 3000);
};
const stopAutoPlay = () => {
if (interval.current !== null) {
clearInterval(interval.current as NodeJS.Timeout);
interval.current = null;
}
};
useEffect(() => {
clearInterval(interval.current as NodeJS.Timeout);
interval.current = null;
let target = imgRef.current as HTMLElement;
console.log(target.childNodes);
if (interval.current !== null) {
stopAutoPlay();
}
if (interval.current === null) { //避免因为定时器什么的缘故导致计时器多开
startAutoplay();
}
//eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentIndex]);
4.2 底部小圆点按钮
这块部分就是将点击按钮所对应的图片转到主页。这个要实现用setCurrentIndex()就可以,但是要加动画,首先需要将目标页先移到上一页或者下一页的位置,准备平移至主页位置。所以,需要预先将目标图移到动画的起始位置。
如上图所示,如果是目标页是当前页的前面几页(目标页的index比currentIndex小)。需要提前把目标页移到上一页的位置,反之,移到下一页的位置。
移完之后设置currentIndex进行重新渲染。
typescript">if (index < currentIndex.currentIndex) {
target.classList.add("preActive”); //移到上一页的位置,class设为preActive
// target1.classList.add("nextActive");
// target1.classList.remove("active");
setTimeout(() => {
setCurrentIndex((pre) => {
console.log(currentIndex);
// let nextIndex = index + 1 === img.length ? 0 : index + 1;
// if(nextIndex === pre.currentIndex) return;
return {
preIndex:
index - 1 === -1 ? img.length - 1 : index - 1,
currentIndex: index,
nextIndex: pre.currentIndex,
};
});
}, 10);
} else if (index > currentIndex.currentIndex) {
target.classList.add("nextActive”); //移到下一页的位置,class设为nextActive
// target1.classList.add("preActive");
// target1.classList.remove("active");
console.log(currentIndex);
setTimeout(() => {
setCurrentIndex((pre) => {
return {
nextIndex:
index + 1 === img.length ? 0 : index + 1,
currentIndex: index,
preIndex: pre.currentIndex,
};
});
}, 10);
}
到此为止出现的问题:
和“自动轮播和前后按钮换图”之前的联动出现的错误是:当跳到前面页面的时候,下一页的类名没有nextActive所以跳到下一页可能没有动画;相同的跳到前面页的时候,上一页的类名没有preActive,可能没有翻页动画。因此,我们要保证当前页的前一页类名有preActive,后一页类名有nextActive.
typescript"><div
key={index}
className={`carousel-item ${
index === currentIndex.currentIndex ? "active" : ""
}${
index ===
(currentIndex.currentIndex - 1 === -1
? img.length - 1
: currentIndex.currentIndex - 1) ||
(index === currentIndex.preIndex )
? "preActive"
: ""
} ${
index ===
(currentIndex.currentIndex + 1 === img.length
? 0
: currentIndex.currentIndex + 1) ||
index === currentIndex.nextIndex
? "nextActive"
: ""
}`}
>
这样写存在的错误是会造成同一个图片有preActive和nextActive的情况
解决方案:按键跳转前的主页是目标页的下一页:因此,当preIndex等于currentIndex+1不给preIndex
typescript"><div
key={index}
className={`carousel-item ${
index === currentIndex.currentIndex ? "active" : ""
}${
index ===
(currentIndex.currentIndex - 1 === -1
? img.length - 1
: currentIndex.currentIndex - 1) ||
//下面这部分代码就是 当preIndex等于currentIndex+1不给preIndex
(index === currentIndex.preIndex &&
currentIndex.preIndex !==
(currentIndex.currentIndex + 1 === img.length
? 0
: currentIndex.currentIndex + 1))
? "preActive"
: ""
} ${
index ===
(currentIndex.currentIndex + 1 === img.length
? 0
: currentIndex.currentIndex + 1) ||
index === currentIndex.nextIndex
? "nextActive"
: ""
}`}
>
五、遇到的问题
typescript"> useEffect(() => {
console.log("停止计时器");
console.log(currentIndex);
clearInterval(interval.current as NodeJS.Timeout);
interval.current = null;
let target = imgRef.current as HTMLElement;
console.log(target.childNodes);
if (interval.current !== null) {
stopAutoPlay();
}
//如果这里这样写会出现问题,如果在这三秒内点的话,就会导致setCurrentIndex的值都变成NaN
setTimeout(()=>{
if (interval.current === null) {
startAutoplay();
}
},3000)
//eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentIndex]);
//改完之后的版本
useEffect(() => {
console.log("停止计时器");
console.log(currentIndex);
clearInterval(interval.current as NodeJS.Timeout);
interval.current = null;
let target = imgRef.current as HTMLElement;
console.log(target.childNodes);
if (interval.current !== null) {
stopAutoPlay();
}
//改成异步函数,或者setTimeout的事件变短一点
if (interval.current === null) {
startAutoplay();
}
//eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentIndex]);
整个项目可以在下面链接下载:https://download.csdn.net/download/weixin_43872912/90429357?spm=1001.2014.3001.5501