React视角下的轮播图

2023-05-22,,

天猫购物网站最显眼的就是轮播图了。我在学习一样新js库,一个新框架或新的编程思想的时候,总是感叹“入门必做选项卡,进阶须撸轮播图。”作为一个React组件,它是状态操控行为的典型,拿来练手是个不错的选择。

为了复习,这次就尝试用原生的javascript+React来完成。


轮播图原生实现

所谓轮播图其实是扩展版的选项卡。

先布局

主干架构

<div id="tabs">
<ul id="btns">
<li><a href="javascript:;"></a></li>
<li><a href="javascript:;"></a></li>
<li><a href="javascript:;"></a></li>
<li><a href="javascript:;"></a></li>
<li><a href="javascript:;"></a></li>
<li><a href="javascript:;"></a></li>
</ul>
<ul id="imgs">
<li><img src="images/banner1.jpg"></li>
<li><img src="images/banner2.jpg"></li>
<li><img src="images/banner3.jpg"></li>
<li><img src="images/banner4.jpg"></li>
<li><img src="images/banner5.jpg"></li>
<li><img src="images/banner6.jpg"></li>
</ul>
</div>

样式如下

/*css-reset*/
*{
margin:0;
padding: 0;
}
ul li{
list-style: none;
}
img{
border: none;
}
a{
text-decoration: none;
} /******************/
#tabs{
width: 1130px;
height: 500px;
margin: 100px auto;
position: relative;
overflow: hidden;
}
#tabs li{
float: left;
}
#tabs img{
width: 1130px;
height: 500px;
}
#btns{
position: absolute;
top:88%;
left:395px;
z-index: 9;
}
#btns a{
display: block;
width: 17px;
height: 17px;
background: rgba(0,0,0,0.3);
border-radius: 50%;
border: 2px solid rgba(0,0,0,0.3);
}
#btns li{
margin: 10px;
}

大概效果

纯javascript实现

事件

一个简单的轮播图包括多个事件。

鼠标移入移出:当鼠标移出,或者是鼠标不在轮播图上面,执行自动播放
当鼠标移入:不再自动播放,而且点击按钮会执行跳转到相应的页面。

渐变

因为6张图不是很多。所以考虑六张图全部做绝对定位,按照顺序叠加在一起。然再通过一个透明度的运动框架,实现之。

在此我选用这个运动框架:

function getStyle(obj,attr){
if(obj.crrentStyle){
return obj.currentStyle[attr];
//兼容IE8以下
}else{
return getComputedStyle(obj,false)[attr];
//参数false已废。照用就好
}
} function startMove(obj,json,fn){
//清理定时器
if(obj.timer){
clearInterval(obj.timer);
}
obj.timer=setInterval(function(){
var bStop=false;//如果为false就停了定时器!
var iCur=0;
// 处理属性值
for(var attr in json){
if(attr=='opacity'){
iCur=parseInt(parseFloat(getStyle(obj,attr))*100);
}else{
iCur=parseInt(getStyle(obj,attr));
}
//定义速度值
var iSpeed=(json[attr]-iCur)/8;
iSpeed=iSpeed>0?Math.ceil(iSpeed):Math.floor(iSpeed);
//检测停止:如果我发现某个值不等于目标点bStop就不能为true。
if(iCur!==json[attr]){
bStop=false;
}
if(attr=='opacity'){
obj.style[attr]=(iCur+iSpeed)/100;
obj.style.filter='alpha(opacity:'+(iCur+iSpeed)+')';
}else{
obj.style[attr]=iCur+iSpeed+'px';
}
}
//检测是否停止,是的话关掉定时器
if(bStop===true){
if(iCur==json[attr]){
clearInterval(obj.timer);
if(fn){
fn();
}
}
}
},30);
}

这个框架可以指定样式值进行渐变。

不得不说,这确实是一个很棒的运动框架。可以把它单独放在为一个名为move.js的文件中再引入。

根据这个思路写出原生的代码:

window.onload=function(){
var oTab=document.getElementById('tabs');
var oBtns=document.getElementById('btns');
var aBtns=document.getElementsByTagName('a');
var oImgs=document.getElementById('imgs');
var aImgsLi=oImgs.getElementsByTagName('li');
var bCheck=true;
var iNow=0; // 以下是初始化设置:
aBtns[0].style.background='rgba(255,255,255,0.5)';
aImgsLi[0].style.zIndex=6; function iNowlistener(){//改变的核心函数
// 初始化
for(var i=0;i<aBtns.length;i++){
aBtns[i].style.background='rgba(0,0,0,0.3)';
}
aBtns[iNow].style.background='rgba(255,255,255,0.5)';
for(var j=0;j<aBtns.length;j++){
aImgsLi[j].style.opacity=0;
if(j!==iNow){
aImgsLi[j].style.display='none';
}else{
aImgsLi[j].style.display='block';
startMove(aImgsLi[j],{'opacity':100});
}
}
} var timer=null;
timer=setInterval(function(){
if(bCheck){
if(iNow==5){//将最后一个变为0
iNow=0;
}else{
iNow++;
}
iNowlistener();
}else{
return false;
}
},2000); oTab.onmouseover=function(){
bCheck=false;
for(var i=0;i<aBtns.length;i++){
aBtns[i].index=i;
aBtns[i].onmouseover=function(){
if(this.index==iNow){
return false;
}else{
iNow=this.index;
iNowlistener();
}
};
} }; oTab.onmouseout=function(){
bCheck=true;
}; };

效果如下:

不得不说,原生的代码写起来好长好长。

很长吗?后面的更长。


React思路

以上原生代码已经经过了初步的封装——比如INowListener。但是在React的价值观来说,显然还需要进一步的封装。甚至重新拆分。最理想的情况是:顶层组件作为主干架构和状态机。下层组件接收状态并运行方法。

多少个组件?

在这个轮播图中,就三个组件。

- Tabs
-imgs
-btns
var Tabs=React.createClass({
render:function(){ return (
<div id="tabs">
<Btns/>
<Imgs/>
</div>
);
}
}); var Btns=React.createClass({
render:function(){
var arr=[];
for(var i=0;i<6;i++){
var btnContent=
<li key={i.toString()}><a href="javascript:;"></a></li>
arr.push(btnContent);
}
return (
<ul id="btns">{arr}</ul>
)
}
}); var Imgs=React.createClass({
render:function(){
var arr=[];
for(var i=0;i<6;i++){
var imgContent=
<li key={i.toString()}><img src={"images/banner"+(i+1)+".jpg"}/></li>
arr.push(imgContent);
console.log(arr)
}
return (
<ul id="imgs">{arr}</ul>
);
}
}); ReactDOM.render(
<Tabs/>,
document.getElementById('example')
)

这样写就把样式写出来了。

哪个是状态?

iNOW是状态。而且是最重要的状态!既然这样,就考虑把状态iNow放顶层。

鼠标悬停看起来也是状态,但悬停按钮上,触发iNow改变——因此还是iNow。

鼠标移入移出事件,应该是状态。但是这个移入移出的状态依赖于iNow。所以不能单独用。

需要哪些props?

构造组件时,为了灵活性,一般都不考虑把组件框架写死。比如图片张数,id名,等等都应该是props。但是这些暂时来说,都是次要的。

状态肯定是一个核心props,此外,底层设置状态的回调也是核心的props之一。

空谈太多无意义,接下来尝试实现!


自动按钮

现在先不考虑其它,单看按钮。

在插入文档之后,开启一个定时器,每隔2000ms执行一次状态更新。

setState的写法

那涉及到了iNow状态根据前一个状态更新,官方文档不建议这种写法:

this.setState({
return {
iNow:this.state.iNow+1
}
})

因为状态更新可能是异步的。这样写很容易出问题。

事实上,官网提供了这样的写法:

this.setState(function(prev,props){
return {
iNow:prev.iNow+1
}
})

在这里只用第一个参数就够了。

想当然的按钮

定时器应该是一个状态计算器。

所以按钮可以这么写:

var Btns=React.createClass({
getInitialState:function(){
return ({
iNow:0
})
},
componentDidMount:function(){
var _this=this;
setInterval(function(){
_this.setState(function(prev){
//console.log(prev)
if(prev.iNow==5){
return {
iNow:0
};
}else{
return {
iNow:prev.iNow+1
};
}
})
},2000);
},
render:function(){
var arr=[];
for(var i=0;i<6;i++){
var btnContent=null;
if(i==this.state.iNow){
btnContent=
<li key={i.toString()}><a style={{background:'rgba(255,255,255,0.5)'}} href="javascript:;"></a></li>
}else{
btnContent=
<li key={i.toString()}><a href="javascript:;"></a></li>
} arr.push(btnContent);
} return (
<ul id="btns">{arr}</ul>
);
}
});

按钮就实现了。

看起来不错,但是这样写可能在未来造成极大的不便。

悬停交互

再强调一次价值观这个概念,按照React的价值观,状态应该从顶层传下去,况且在这个案例中,顶层Tabs组件做一件事就够了:状态机,在Btn组件插入到文档之后,打开这个定时器。底层组件比如Btns根据状态每隔2000ms通过props刷新变化。

同时,我还要实现一个简单的交互功能:当鼠标悬停在Tabs上时,不再允许iNow自动更新。——可以做一个bCheck开关,当Tabs组件鼠标移入/移出时,触发bCheck的来回变化。

此处可能有个小问题,就是鼠标一道按钮组上时,会造成bCheck抖动。但是最后又变回false。所以认为不影响。

很自然想到,bCheck为false时,关闭定时器。但是这样做又等于浪费了定时器的功能,回调方法中一旦关掉定时器,再重新定时器就不是一般的麻烦了,为什么不直接在定时器做判断呢?所以我认为不应该让定时器停下来。只需要改变定时器计算iNow的行为就行了。

var Tabs=React.createClass({
getInitialState:function(){
return {
iNow:0,
bCheck:true//为false时不允许定时器计算更新iNow
}
},
setInow:function(){
var _this=this;
var timer=setInterval(function(){
if(_this.state.bCheck){
//console.log(_this.state.bCheck)
_this.setState(function(prev){
if(prev.iNow==5){
return {
iNow:0
};
}else{
return {
iNow:prev.iNow+1
};
}
});
}else{
console.log('该停了!')
return false;
} },2000); },
checkSwitch:function(){
this.setState(function(prev){
return {
bCheck:!prev.bCheck,
}
})
},
render:function(){
return (
<div id="tabs" onMouseOver={this.checkSwitch} onMouseOut={this.checkSwitch}>
<Btns iNow={this.state.iNow} setInow={this.setInow}/>
<Imgs/>
</div>
);
}
}); var Btns=React.createClass({
componentDidMount:function(){
this.props.setInow();//插入后就执行回调方法
},
render:function(){
var arr=[];
for(var i=0;i<6;i++){
var btnContent=null;
if(i==this.props.iNow){
btnContent=
<li key={i.toString()}>
<a style={{background:'rgba(255,255,255,0.5)'}} href="javascript:;"></a>
</li>
}else{
btnContent=
<li key={i.toString()}><a href="javascript:;"></a></li>
} arr.push(btnContent);
} return (
<ul id="btns">{arr}</ul>
);
}
});

图片动画

一件事三个步骤

图片组件虽说只是做一件事情(根据iNow渲染效果),但是也得分三步来做。

首先,渲染前应该保证索引值非iNow的所有图片display为none。索引值为iNow的图片透明度为0。(初始化)

其次,在首次插入文档完毕之后(componentDidMount),对第0张图执行startMove函数。

第三,需要一个监听顶层iNow的方法。定时器已经给Btns组件用了,再用就会出错。

留意到Imgs组件实际上只接受一个会变化的props那就是iNow。因此采用componentWillReceiveProps

生命周期方法

componentWillReceiveProps

组件接收到新的props时调用,并将其作为参数nextProps使用,此时可以更改组件propsstate

    componentWillReceiveProps: function(nextProps) {
if (nextProps.bool) {
this.setState({
bool: true
});
}
}

这里采用的两个组件周期方法都是组件真实存在时的方法。所以可以直接使用真实的DOM命令。

实现

var Tabs=React.createClass({
getInitialState:function(){
return {
iNow:0,
bCheck:true
};
},
setInow:function(){
var _this=this;
var timer=setInterval(function(){
if(_this.state.bCheck){
//console.log(_this.state.bCheck)
_this.setState(function(prev){
if(prev.iNow==5){
return {
iNow:0
};
}else{
return {
iNow:prev.iNow+1
};
}
});
}else{
console.log('该停了!')
return false;
} },2000); },
checkSwitch:function(){
console.log(this.state.bCheck)
this.setState(function(prev){
return {
bCheck:!prev.bCheck
};
});
},
render:function(){
return (
<div id="tabs"
onMouseOver={this.checkSwitch}
onMouseOut={this.checkSwitch}>
<Btns iNow={this.state.iNow}
setInow={this.setInow} />
<Imgs iNow={this.state.iNow}/>
</div>
);
}
}); var Btns=React.createClass({
componentDidMount:function(){
this.props.setInow();
}, render:function(){
var arr=[];
for(var i=0;i<6;i++){
var btnsContent=null;
if(i==this.props.iNow){
btnsContent=
<li key={i.toString()}>
<a style={{background:'rgba(255,255,255,0.5)'}} href="javascript:;"></a>
</li>
}else{
btnsContent=
<li key={i.toString()}>
<a href="javascript:;"></a>
</li>
}
arr.push(btnsContent);
} return (
<ul id="btns">{arr}</ul>
);
}
}); var Imgs=React.createClass({
componentDidMount:function(){//刚开始加载时,就执行动画函数
var iNow=this.props.iNow;
var obj=document.getElementById('imgs').getElementsByTagName('li')[iNow].childNodes[0];
startMove(obj,{'opacity':100});
},
componentWillReceiveProps:function(nextProps){
var obj=document.getElementById('imgs').getElementsByTagName('li')[nextProps.iNow].childNodes[0];
//console.log(obj)
startMove(obj,{'opacity':100});
},
// this.startMove:startMove(),
render:function(){
var arr=[];
for(var i=0;i<6;i++){
var imgsContent=null
if(i==this.props.iNow){
imgsContent=
<li key={i.toString()}>
<img style={{opacity:'0'}} src={'images/banner'+(i+1)+'.jpg'} />
</li>
arr.push(imgsContent);
}else{
imgsContent=
<li key={i.toString()}>
<img style={{display:'none'}} src={'images/banner'+(i+1)+'.jpg'} />
</li>
arr.push(imgsContent);
} } return (
<ul id="imgs">{arr}</ul>
)
}
}) ReactDOM.render(
<Tabs/>,
document.getElementById('example')
);

看起来Imgs组件已经很完备了。——就它的功能来说已经没有什么需要添加了。


鼠标悬停改变iNow

这个事件只能在底层组件Btns上实现。所以要拿到悬停的索引值。

然后通过回调,把该按钮的索引值设置为整个组件Tabs的状态iNow。

为了干这两件事,还是用一个changeInow(e)函数来包装它们。

给谁绑定?加什么事件?

为了忠实原来的代码。我给a标签加onMouseOver事件。

加了事件直接,秉承这React的核心价值观(一个组件只干一件事),我把get到的index值通过this.props.setInow传递回去。只要顶层的iNow变了,下面的组件不管什么状态,都会乖乖听话了。

如何获取当前悬停的索引值?

在Jquery很容易使用index方法来获取索引值。但是在原生方法中,还得费一番周章。

给所有a绑定一个onMouseOver事件,假设该事件方法的参数为e,那么e.target就是该参数的方法。

这需要写一个getIndex方法

...
getIndex:function(e){
var list=e.target.parentNode.parentNode.childNodes;
for(var i=0;i<list.length;i++){
if(list[i]===e.target.parentNode){
return i;
}
}
},
...

拿到索引值之后

——就把它设置为顶层的iNow。

既然决定通过this.props.setInow回调,那么还得传一个索引值参数,回到顶层稍微修改下方法,就实现了。

全部代码:

var Tabs=React.createClass({//顶层组件
getInitialState:function(){
return {
iNow:0,
bCheck:true
};
},
setInow:function(index){//核心状态计算工具:依赖定时器进行实时刷新
if(index!==undefined){//如果参数有内容。
this.setState({
iNow:index
});
}else{
var _this=this;
this.timer=setInterval(function(){
if(_this.state.bCheck){
//console.log(_this.state.bCheck)
_this.setState(function(prev){
if(prev.iNow==5){
return {
iNow:0
};
}else{
return {
iNow:prev.iNow+1
};
}
});
}else{
//console.log('该停了!')
return false;
}
},2000);
}
},
checkSwitch:function(){
//console.log(this.state.bCheck)
this.setState(function(prev){
return {
bCheck:!prev.bCheck
};
});
},
render:function(){
return (
<div id="tabs"
onMouseOver={this.checkSwitch}
onMouseOut={this.checkSwitch}>
<Btns iNow={this.state.iNow}
setInow={this.setInow} />
<Imgs iNow={this.state.iNow}/>
</div>
);
}
}); var Btns=React.createClass({
componentDidMount:function(){
this.props.setInow();
},
getIndex:function(e){//获取a的父级索引值
var list=e.target.parentNode.parentNode.childNodes;
for(var i=0;i<list.length;i++){
if(list[i]===e.target.parentNode){
return i;
}
}
},
changeInow:function(e){//回调方法
//console.log($(e.target).parent().index());
//console.log(this.getIndex(e));
var index=this.getIndex(e);
this.props.setInow(index)
}, render:function(){
var arr=[];
for(var i=0;i<6;i++){
var btnsContent=null;
var index=i;
if(i==this.props.iNow){
btnsContent=
<li key={i.toString()}>
<a onMouseOver={this.changeInow} style={{background:'rgba(255,255,255,0.5)'}} href="javascript:;"></a>
</li>
}else{
btnsContent=
<li key={i.toString()}>
<a onMouseOver={this.changeInow} href="javascript:;"></a>
</li>
}
arr.push(btnsContent);
} return (
<ul id="btns">{arr}</ul>
);
}
}); var Imgs=React.createClass({
componentDidMount:function(){//刚开始加载时,就执行动画函数
var iNow=this.props.iNow;
var obj=document.getElementById('imgs').getElementsByTagName('li')[iNow].childNodes[0];
startMove(obj,{'opacity':100});
},
componentWillReceiveProps:function(nextProps){
var obj=document.getElementById('imgs').getElementsByTagName('li')[nextProps.iNow].childNodes[0];
//console.log(obj)
startMove(obj,{'opacity':100});
},
// this.startMove:startMove(),
render:function(){
var arr=[];
for(var i=0;i<6;i++){
var imgsContent=null
if(i==this.props.iNow){
imgsContent=
<li key={i.toString()}>
<img style={{opacity:'0'}} src={'images/banner'+(i+1)+'.jpg'} />
</li>
arr.push(imgsContent);
}else{
imgsContent=
<li key={i.toString()}>
<img style={{display:'none'}} src={'images/banner'+(i+1)+'.jpg'} />
</li>
arr.push(imgsContent);
} } return (
<ul id="imgs">{arr}</ul>
)
}
}) ReactDOM.render(
<Tabs/>,
document.getElementById('example')
);

完善

我们要让这个组件可复用,换言之就是把之前写死的东西比如图片数量,样式,id名都变成Tabs的props属性。

这下工作量够了吧。原来50多行的东西改写完之后大概150多行。

var Tabs=React.createClass({//顶层组件
getInitialState:function(){
return {
iNow:0,
bCheck:true
};
},
setInow:function(index){//核心状态计算工具:依赖定时器进行实时刷新
if(index!==undefined){//如果参数有内容。
this.setState({
iNow:index
});
}else{
var _this=this;
this.timer=setInterval(function(){
if(_this.state.bCheck){
//console.log(_this.state.bCheck)
_this.setState(function(prev){
if(prev.iNow==this.props.nums-1){
return {
iNow:0
};
}else{
return {
iNow:prev.iNow+1
};
}
});
}else{
//console.log('该停了!')
return false;
}
},this.props.timer);
}
},
checkSwitch:function(){
//console.log(this.state.bCheck)
this.setState(function(prev){
return {
bCheck:!prev.bCheck
};
});
},
render:function(){
return (
<div id={this.props.idNames.main}
onMouseOver={this.checkSwitch}
onMouseOut={this.checkSwitch}> <Btns iNow={this.state.iNow}
setInow={this.setInow}
nums={this.props.nums}
idNames={this.props.idNames} /> <Imgs iNow={this.state.iNow}
nums={this.props.nums}
idNames={this.props.idNames}
imgType={this.props.imgType} /> </div>
);
}
}); var Btns=React.createClass({
componentDidMount:function(){
this.props.setInow();
},
getIndex:function(e){//获取a的父级索引值
var list=e.target.parentNode.parentNode.childNodes;
for(var i=0;i<list.length;i++){
if(list[i]===e.target.parentNode){
return i;
}
}
},
changeInow:function(e){//回调方法
//console.log($(e.target).parent().index());
//console.log(this.getIndex(e));
var index=this.getIndex(e);
this.props.setInow(index)
}, render:function(){
var arr=[];
for(var i=0;i<this.props.nums;i++){
var btnsContent=null;
var index=i;
if(i==this.props.iNow){
btnsContent=
<li key={i.toString()}>
<a onMouseOver={this.changeInow} id={this.props.idNames.active} href="javascript:;"></a>
</li>
}else{
btnsContent=
<li key={i.toString()}>
<a onMouseOver={this.changeInow} href="javascript:;"></a>
</li>
}
arr.push(btnsContent);
} return (
<ul id={this.props.idNames.btns}>{arr}</ul>
);
}
}); var Imgs=React.createClass({
componentDidMount:function(){//刚开始加载时,就执行动画函数
var iNow=this.props.iNow;
var obj=document.getElementById(this.props.idNames.imgs).getElementsByTagName('li')[iNow].childNodes[0];
startMove(obj,{'opacity':100});
},
componentWillReceiveProps:function(nextProps){//每当收到新的props就执行动画
var obj=document.getElementById(this.props.idNames.imgs).getElementsByTagName('li')[nextProps.iNow].childNodes[0];
//console.log(obj)
startMove(obj,{'opacity':100});
}, render:function(){
var arr=[];
for(var i=0;i<this.props.nums;i++){
var imgsContent=null;
var src=this.props.imgType.url+this.props.imgType.name+(i+1)+'.'+this.props.imgType.type;
if(i==this.props.iNow){
imgsContent=
<li key={i.toString()}>
<img style={{opacity:'0'}} src={src} />
</li>
arr.push(imgsContent);
}else{
imgsContent=
<li key={i.toString()}>
<img style={{display:'none'}} src={src} />
</li>
arr.push(imgsContent);
}
} return (
<ul id={this.props.idNames.imgs}>{arr}</ul>
)
}
}) ReactDOM.render(
<Tabs
nums={6}
timer={2000}
idNames={
{
main:"tabs",
btns:"btns",
imgs:"imgs",
active:"btn-active"
}
}
imgType={
{
type:"jpg",
url:"images/",
name:"banner"
}
}
/>,
document.getElementById('example')
);

其中多设置了一个#btn-active样式。

#btn-active{
background:rgba(255,255,255,0.5)!important;
}

是不是好长好长呢?

demo地址:

http://djtao.top/tabs/

但是这个确实是一个可复用的,而且还是原生js写成的组件。

不得不说,作为一个初学几天的人,写这东西时的时候遭遇好多的坑。但是最后“蓦然回首,那人却在灯火阑珊处”,也有种人生三境界的感悟了!

React视角下的轮播图的相关教程结束。

《React视角下的轮播图.doc》

下载本文的Word格式文档,以方便收藏与打印。