這篇筆記是官方教程的延續筆記,所有代碼基於第一篇筆記的結尾代碼。旨在解決教程後面提出的五個問題。 一 . 用(X,Y)來取代原有的數字坐標 原來的數字坐標是這樣的: 現在的要求是把原來的代碼坐標改為二維坐標系的表達形式,並且在歷史記錄面板中打出轉換後的坐標。 如果只是為了輸出好看。只需要寫一個轉換方 ...
這篇筆記是官方教程的延續筆記,所有代碼基於第一篇筆記的結尾代碼。旨在解決教程後面提出的五個問題。
一 . 用(X,Y)來取代原有的數字坐標
原來的數字坐標是這樣的:
現在的要求是把原來的代碼坐標改為二維坐標系的表達形式,並且在歷史記錄面板中打出轉換後的坐標。
如果只是為了輸出好看。只需要寫一個轉換方法,這些在頂層的Game
組件中實現就夠了。而不需要修改原來的代碼核心。
很自然想到用switch語句,其實都行,怎麼喜歡怎麼寫。
convert:function(i){//i是一維坐標
var x=Math.floor(i/3)+1;
var y=0;
if((i+1)%3===0){
y=3;
}else{
y=(i+1)%3;
}
return [x,y];
},
調用就直接在渲染前調用。把它存到state裡面去。
接下來問題是把這個方法怎麼獲取參數i,最直接的辦法是寫一個全局變數,然後從handleClick裡面拿到i。但是全局變數不環保。或許再設一個頂層狀態lastLocation
是個不錯的選擇,渲染隊列是一個數組,姑且稱之為historyLocation
。
根據React的價值觀,能根據其它原有狀態計算出來的東西,就不需要設置額外的狀態。但如果思路不明確,就在這裡先寫出來。
回退步驟的本質
在上一篇筆記最後,官方文檔沒有說清楚一個問題。就是狀態中的stepNumber是什麼。現在再次遇到,需要寫明白給自己提個醒。
回退步驟
每走一步,history
狀態就會在最後追加一個最新版本。
stepNumber
實際上是一個指針,根據這個指針,發送history狀態的版本(可能是舊的,也可能是最新的),用它來調控渲染狀態。
點擊回退步驟,就是把指針往前挪。
通過暫存器刷新狀態
如果沒有任何其它操作直接觸發handleClick,stepNumber
指針直接指向最新的版本。
如果在回退步驟上發生了handleClick,那麼將發生以下事情:
- 根據指針生成若幹個狀態暫存器,這個暫存器是獨立且不具備任何效力的,拋開環境來看就是普通變數;
- 追加新的狀態到暫存器;
- 再用這個暫存器替換掉原有的狀態,在此,回退步驟列表將被刷新。
究竟有幾個狀態暫存器?在這裡就兩個:
- 一個儲存history的當前指針版本:
javascript handleClick:function(i){ //history指針版本暫存 var history=this.state.history.slice(0,this.state.stepNumber+1); var lastHistory=history[this.state.stepNumber]; var squares=lastHistory.squares.slice();
- 一個儲存二維坐標軸版本:
javascript /**上接handleClick***/ //二維坐標數據暫存器 var historyLocation=this.state.historyLocation; historyLocation=historyLocation.slice(0,this.state.stepNumber); historyLocation.push(this.convert(i));//刷新狀態暫存器
通過狀態暫存器,既可以在指針位置重新開始,又能在屏幕上保留歷史步驟數方便查看,即實現官方文檔所謂的“時間旅行”。
在這裡意識到狀態暫存器其實應該只有一個是最好的。
judgeWinner
的完善
按理來說,判斷勝負的函數judgeWinner
應該是在組件的裡面,這樣比較環保,也可以更好地調用組件中的狀態。
現在就把它拿進來。直接生成渲染方法中的status
。並且添加一個和棋的判斷。實現思路是調用指針版本的history狀態數組。然後遍歷這個數組對象,如果發現9個位置全部不為null,就返回和棋。
放進來之後,參數也沒有必要再寫,全部改為state相關的表達。
這樣一來就沒辦法用原來的禁著點判斷了。因為不好判斷棋局是否完結。在此根據返回的結果進行indexOf判斷,留下的坑後面填。
所以到此為止,Game組件應該是:
getInitialState:function(){
return {
history:[
{squares:Array(9).fill(null)}
],
turnToX:true,
stepNumber:0,
historyLocation:[]
}
},
// 判斷勝負的函數,窮舉法
judgeWinner:function(){
var history=this.state.history;
var lastHistory=history[this.state.stepNumber];
var squares=lastHistory.squares;
var win=[
[0,1,2],
[0,3,6],
[0,4,8],
[1,4,7],
[2,5,8],
[2,4,6],
[3,4,5],
[6,7,8]
];
for(var i=0;i<win.length;i++){
var winCase=win[i];
if(squares[winCase[0]]&&squares[winCase[0]]===squares[winCase[1]]&&squares[winCase[1]]===squares[winCase[2]]){//三子一線
return ('獲勝方是:'+squares[winCase[0]]);//返回勝利一方的標識
}
}
// 定義當前棋盤上被填滿的格子數量
var fill=lastHistory.squares.filter((item)=>item!=null).length;
if(fill==9){
return '和棋!'
}else{
var player=this.state.turnToX?'X':'O';
return ('輪到'+player+'走');
}
},
// 點擊事件是把暫存器的內容存為真正的狀態。
handleClick:function(i){
//歷史squares暫存
var history=this.state.history;
history=history.slice(0,this.state.stepNumber+1);
var lastHistory=history[this.state.stepNumber];
var winner=this.judgeWinner();
var squares=lastHistory.squares.slice();
//歷史步驟暫存器
var historyLocation=this.state.historyLocation;
historyLocation=historyLocation.slice(0,this.state.stepNumber);
historyLocation.push(this.convert(i));
if((winner.indexOf('輪到')==-1)||squares[i]){
return false;
//勝負已分或是已有子則不可落子。indexOf這是一種暫時的非主流寫法
}
// 判斷下棋的輪換色
squares[i]=this.state.turnToX?'X':'O';
this.setState({
history:history.concat([{squares:squares}]),
turnToX:!this.state.turnToX,
stepNumber:history.length,
historyLocation:historyLocation
});
},
// 歷史步驟跳轉是把狀態還原到某個時間點,狀態根據stepNumber呈現內容,但不會改變最終狀態。
jumpTo:function(step){
this.setState({
stepNumber:step,
turnToX:step%2?false:true
});
},
// 坐標轉換函數
convert:function(i){
var x=Math.floor(i/3)+1;
var y=0;
if((i+1)%3===0){
y=3;
}else{
y=(i+1)%3;
}
return [x,y];
},
render:function(){
var history=this.state.history.slice();
var lastHistory=history[this.state.stepNumber];//渲染方法遵照的是stepNumber而不是最後一步
var status=this.judgeWinner();//獲勝狀態
var arr=[];
var location=this.state.historyLocation.slice();
var _this=this;
history.map(function(step,move){
var content='';
if(move!==0){
content='Move#'+move+':'+'('+location[move-1][0]+','+location[move-1][1]+')';
//console.log(location[move-1])
}else{
content='游戲開始~';
}
arr.push(<li key={move}><a onClick={()=>_this.jumpTo(move)} href="javascript:;">{content}</a></li>);
});
return (
<div className="game">
<Board lastHistory={lastHistory.squares} onClick={(i)=>this.handleClick(i)} />
<div className="info">
<div>{status}</div>
<ol>{arr}</ol>
</div>
</div>
);
}
});
二. 對右方的被選中的當前記錄進行加粗顯示
樣式這種東西,就交給CSS來實現吧!
.back-active{
font-weight: bolder;
color: #EE9611;
}
簡單實現
思路就是加個class。操作方法是jumpTo。
問題在於,當前的jumpTo已經給定了參數。為了拿到e.target
還得在改改。
jumpTo在這個問題中實際上要完成兩件事,刪除所有a的class中可能.back-active
;給當前對象加個.back-active
。
有了e.target,就能用DOM找到該有的內容。比方說e.target.parentNode.parentNode.childNode
就代表所有點擊對象上層的所有li集合
然而這個集合不是一個數組啊,不能map。只能用for迴圈。根據查到的性能資料,for迴圈還真的比其它迭代方法高。
jumpTo:function(e,step){
// console.log(e.target)
var aList=e.target.parentNode.parentNode.childNodes;
for(var i=0;i<aList.length;i++){
var item=aList[i];
if(item.childNodes[0].classList.contains('back-active')){
item.childNodes[0].classList.remove('back-active');
}
}
e.target.classList.add('back-active');
this.setState({
stepNumber:step,
turnToX:step%2?false:true
});
},
這個問題就算解決了。
點擊實現高亮當前的步驟
其實就個人理解來說,不應該再對handleClick再加什麼高亮當前步驟的操作了。當前步驟明擺著就是最後一個。縱觀就當前的代碼實現,用戶體驗已經很好了,進程不會亂七八糟,用戶還可以很清晰地知道指針指向的還原點。還高亮什麼?
但是假設老闆就要求點擊按鈕時最後一步也高亮,那也只能照做。
顯然,這個應該放渲染前判斷:如果這是狀態最後一步(是this.state.history.length-1
,不是this.state.stepNumber
),那麼就高亮。反正樣式也不要錢,就多寫一個樣式給它。
.process-active{
font-weight: bolder;
color: green;
}/*寫在.back-active之後,方便覆蓋*/
這樣,渲染前的處理里還得多加一個判斷:是最後一個就加.process-active
——這段獲取歷史步驟的方法已經變得太長了。為了閱讀方便把它放一個getMoveList
函數里吧。
...
getMoveList:function(){
var history=this.state.history.slice();
var arr=[];
var location=this.state.historyLocation.slice();
var _this=this;
history.map(function(step,move){
var content='';
if(move!==0){
content='Move#'+move+':'+'('+location[move-1][0]+','+location[move-1][1]+')';
//console.log(location[move-1])
}else{
content='游戲開始~';
}
//console.log(_this.state.stepNumber)
if(arr.length==_this.state.history.length-1){
arr.push(<li key={move}><a className="process-active" onClick={(e)=>_this.jumpTo(e,move)} href="javascript:;">{content}</a></li>);
}else{
arr.push(<li key={move}><a onClick={(e)=>_this.jumpTo(e,move)} href="javascript:;">{content}</a></li>);
}
});
return arr;
},
...
這樣,第二個問題就解決了。
三. 用兩個迴圈重寫Board組件,替代掉原來生硬的代碼結構
因為只有9宮格,復用也毫無意義。所以寫死也問題不大。
想到的處理方法就是這樣了。
var Board=React.createClass({
renderSquare:function(i){
return <Square key={i} value={this.props.lastHistory[i]} onClick={() => this.props.onClick(i)} />
},
getSquare:function(rows){
var index=rows*3;
var arr=[];
for(var i=index;i<index+3;i++){
arr.push(this.renderSquare(i));
}
return arr;
},
getBoardRow:function(){
var arr=[];
for(var i=0;i<3;i++){
arr.push(<div key={i} className="board-row">{this.getSquare(i)}</div>)
}
return arr;
},
render:function(){
return (
<div clasName="board">
<div className="status"></div>
{this.getBoardRow()}
</div>
);
}
});
四. 對你的歷史記錄進行升降序排列
接下來又回到Game組件上面來了。在渲染結構中加一個按鈕。點擊,觸發事件。大概就是這樣。
<input type="button" value={this.state.isAscending} onClick={this.sortToggle} />
因為預設就是降序,因此這個toggleSort只做一件事:切換開關。至於是升序還是降序,又要多設置一個開關狀態(isAscending,初始為降序排列)。
根據這個狀態,getMoveList方法決定生成數組後是直接return還是return arr.reverse()
。
sortToggle:function(){
this.setState(function(prevState){
var sort=prevState.isAscending;
var content='';
if(sort=='升序排列'){
content='降序排列';
}else{
content='升序排列'
}
return {
isAscending:content
}
})
},
然後再到getMoveList
方法的最後,加一個判斷:
.....
if(this.state.isAscending=='降序排列'){
return arr;
}else{
return arr.reverse();
}
}
嗯,第四個問題解決。
五. 高亮顯示獲勝的結果
擴展judgeWinner的功能
judgeWinner
判斷函數已經被納入到了組件中,而且只是返回一個status,現在要擴展它的功能,把勝負情況反應出來。
在原來的判斷勝負函數裡面加個console就可以知道勝負手了。
for(var i=0;i<win.length;i++){
var winCase=win[i];
if(squares[winCase[0]]&&squares[winCase[0]]===squares[winCase[1]]&&squares[winCase[1]]===squares[winCase[2]]){//三子一線
console.log(winCase)//這裡的winCase就是勝負手
return ('獲勝方是:'+squares[winCase[0]]);//返回勝利一方的標識
}
}
既然是擴展功能,再來大改就沒必要了。可以考慮把return一個字元串改為return一個數組。第0項放status,第1項放winCase或null
。
有了這個方法,handleClick中那種非主流寫法就可以刪掉了。
var winner=this.judgeWinner();
if(winner[1]||squares[i]){
return false;
//勝負已分或已有子:則不可落子。
}
傳遞勝負手
再寫一個 CSS
.win-case{
color: red;
}
現在可以通過winner[1]拿到勝負手了。它是一個數組。現在就得在Game組件render方法裡面在var一個數據。通過props傳下去,傳到Board組件之後,做一個判斷,看看參數是否符合點位條件,是的話就繼續把class名傳下去。
/********<Game/>*******/
render:function{
var winCase=this.judgeWinner()[1];//獲勝狀態
return (
<div className="game">
<Board winCase={winCase} lastHistory={lastHistory.squares} onClick={(i)=>this.handleClick(i)} />
...
/**********<Board/>***********/
renderSquare:function(i){
if(this.props.winCase){
for(var j=0;j<this.props.winCase.length;j++){
if(this.props.winCase[j]==i){
return <Square winCase="win-case" key={i} value={this.props.lastHistory[i]} onClick={() => this.props.onClick(i)} />
}
}
}
return <Square key={i} value={this.props.lastHistory[i]} onClick={() => this.props.onClick(i)} />
},
...
/************<square/>******************/
var Square=React.createClass({
render:function(){
if(this.props.winCase){
return (
<button className={"square "+this.props.winCase} onClick={() => this.props.onClick()}>{this.props.value}</button>
);
}else{
return (
<button className="square" onClick={() => this.props.onClick()}>{this.props.value}</button>
);
}
}
});
。。。。。。
那麼第五個問題就完成了。
結束
現在,功能已經完備。思路已經理清。再看之前留下的大坑:historyLocation
。
之前提到過,historyLocation
是可以和history
相互計算得出的。historyLocation
只用於展示步數。組件的判斷引擎是用相容history
的一維數組實現的,為了後期實現AI書寫方便,也顯然是history
更好。還是刪掉這個historyLocation。
不好之處在於每次都要多一點計算,相比React每次動輒重新渲染,這點計算也不是很多。
寫一個根據history獲取坐標的方法,拿到坐標之後在轉換為二維坐標,這本質上是一件事,所以convert方法也可以刪掉了。
getRectangular:function(){
var arr=[];
var mainArr=this.state.history.slice();
for(var i=0;i<mainArr.length;i++){
if(i<mainArr.length-1){
for(var j=0;j<9;j++){
//比較mainArr[i].squares和mainArr[i+1].squares[j])不同,拿到坐標值
if(mainArr[i].squares[j]!==mainArr[i+1].squares[j]){
arr.push(j);
}
}
}
}
var result=[]
for(var i=0;i<arr.length;i++){
var x=Math.floor(arr[i]/3)+1;
var y=(arr[i]+1)%3===0?3:(arr[i]+1)%3;
result.push([x,y])
}
return result;
},
可以再自己優化下演算法和css,或者加個重置button之類的。把不必要的變數刪掉。
效果:
下一篇筆記將解決最大的一個坑。
附錄:組件代碼
var Game=React.createClass({
getInitialState:function(){
return {
history:[
{squares:Array(9).fill(null)}
],
turnToX:true,
stepNumber:0,
isAscending:'降序排列'
}
},
// 判斷勝負的函數,窮舉法,返回一個數組,如果勝負已定,第二個元素就是勝負手
judgeWinner:function(){
var lastHistory=this.state.history[this.state.stepNumber];//獲取指針版本
var squares=lastHistory.squares;
var win=[
[0,1,2],
[0,3,6],
[0,4,8],
[1,4,7],
[2,5,8],
[2,4,6],
[3,4,5],
[6,7,8]
];
for(var i=0;i<win.length;i++){
var winCase=win[i];
if(squares[winCase[0]]
&&squares[winCase[0]]===squares[winCase[1]]
&&squares[winCase[1]]===squares[winCase[2]]){//三子一線
return [('獲勝方是:'+squares[winCase[0]]),winCase];//返回一個status和勝負情況
}
}
// 獲取當前棋盤上被填滿的格子數量
var fill=lastHistory.squares.filter((item)=>item!=null).length;
if(fill==9){
return ['和棋!',null];
}else{
var player=this.state.turnToX?'X':'O';
return [('輪到'+player+'走'),null];
}
},
// 點擊事件是把暫存器的內容存為真正的狀態。
handleClick:function(i){
//history指針版本暫存
var history=this.state.history.slice(0,this.state.stepNumber+1);
var lastHistory=history[this.state.stepNumber];
var squares=lastHistory.squares.slice();
var winner=this.judgeWinner();
if(winner[1]||squares[i]){
return false;
//勝負已分或是已有子則不可落子。
}
// 判斷下棋的輪換色
squares[i]=this.state.turnToX?'X':'O';
//覆蓋掉原來的狀態!
this.setState({
history:history.concat([{squares:squares}]),
turnToX:!this.state.turnToX,
stepNumber:history.length
});
},
// 轉化history狀態為各個版本的平面直角坐標
getRectangular:function(){
var arr=[];
var mainArr=this.state.history.slice();
for(var i=0;i<mainArr.length;i++){
if(i<mainArr.length-1){
for(var j=0;j<9;j++){
//比較mainArr[i].squares和mainArr[i+1].squares[j])不同,拿到坐標值
if(mainArr[i].squares[j]!==mainArr[i+1].squares[j]){
arr.push(j);
}
}
}
}
var result=[]
for(var i=0;i<arr.length;i++){
var x=Math.floor(arr[i]/3)+1;
var y=(arr[i]+1)%3===0?3:(arr[i]+1)%3;
result.push([x,y])
}
return result;
},
// 歷史步驟跳轉是把狀態還原到某個時間點,狀態根據stepNumber呈現內容,但不會改變最終狀態。
jumpTo:function(e,step){
var aList=e.target.parentNode.parentNode.childNodes;
for(var i=0;i<aList.length;i++){
var item=aList[i];
if(item.childNodes[0].classList.contains('back-active')){
item.childNodes[0].classList.remove('back-active');
}
}