天貓購物網站最顯眼的就是輪播圖了。我在學習一樣新js庫,一個新框架或新的編程思想的時候,總是感嘆“入門必做選項卡,進階須擼輪播圖。”作為一個React組件,它是狀態操控行為的典型,拿來練手是個不錯的選擇。 為了複習,這次就嘗試用原生的javascript+React來完成。 輪播圖原生實現 所謂輪播 ...
天貓購物網站最顯眼的就是輪播圖了。我在學習一樣新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
使用,此時可以更改組件props
及state
。
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