React的井字過三關(1)


React的井字過三關(1)

本文系React官方教程的Tutorial: Intro To React的筆記。由筆者用ES5語法改寫。

在本篇筆記中,嘗試用React構建一個可交互的井字棋游戲。


開始

先布局:

status反映游戲信息。九宮格采用flex布局。右側有一處游戲信息。

<div id="container">
        <div class="game">
            <div class="board">
                <div class="status">Next player: X</div>
                <div class="board-row">
                    <button class="square"></button>
                    <button class="square"></button>
                    <button class="square"></button>
                </div>
                <div class="board-row">
                    <button class="square"></button>
                    <button class="square"></button>
                    <button class="square"></button>
                </div>
                <div class="board-row">
                    <button class="square"></button>
                    <button class="square"></button>
                    <button class="square"></button>
                </div>
            </div>
            <div class="info">
                <div></div>
                <ol></ol>
            </div>
        </div>
    </div>

再把css寫一下:

/*Simple CSS-Reset*/
*{
	margin:0;
	padding:0;
}

body{
  font: 30px "Century Gothic", Futura, sans-serif;
  margin: 20px;
}

ul{
	list-style: none;
}

a{
	text-decoration: none;
}



ol, ul{
  padding-left: 30px;
}

/*major*/
#container{
	width: 500px;
	margin:0 auto;
}

.game{
  display: flex;
  flex-direction: row;
}

.status{
  margin-bottom: 20px;
  text-align: center;
}

.board-row:after{
  clear: both;
  content: "";
  display: table;
}

.square{
  background: #fff;
  border: 1px solid #999;
  float: left;
  font-size: 36px;
  font-weight: bold;
  line-height: 100px;
  height: 100px;
  margin-right: -1px;
  margin-top: -1px;
  padding: 0;
  text-align: center;
  width: 100px;
}

#container .square:focus {
  background: #ddd;
  outline: none;
}

.info {
  margin-left: 30px;
  font-size:20px;
}

基本效果:

接下來只需要考慮javascript實現就可以了。

整個應用分為三個組件:

  • Square(方塊)
  • Board(九宮格面板)
  • Game(整個游戲)

接下來就是把這個結構用React寫出來。

var Game=React.createClass({
            render:function(){
                return (
                    <div className="game">
                        <Board />
                        <div className="info">
                            <div></div>
                            <ol></ol>
                        </div>
                    </div>
                );
            }
        });

        var Board=React.createClass({
            renderSquare:function(i){
                return   <Square />
            },
            render:function(){
                return (
                    <div clasName="board">
                        <div className="status">Next player: X</div>
                        <div className="board-row">
                            {this.renderSquare(0)}
                            {this.renderSquare(1)}
                            {this.renderSquare(2)}
                        </div>
                        <div className="board-row">
                            {this.renderSquare(3)}
                            {this.renderSquare(4)}
                            {this.renderSquare(5)}
                        </div>
                        <div className="board-row">
                            {this.renderSquare(6)}
                            {this.renderSquare(7)}
                            {this.renderSquare(8)}
                        </div>
                    </div>
                );
            }
        });

        var Square=React.createClass({
            render:function(){
                return (
                    <button className="square"></button>
                );
            }
        });

        ReactDOM.render(
            <Game />,
            document.getElementById('container')
        );

通過props傳遞數據

現在嘗試從Board組件中傳遞一些數據給Square組件:

var Board=React.createClass({
            renderSquare:function(i){
                return   <Square value={i} />
            },
  			...

Square內部:

var Square=React.createClass({
            render:function(){
                return (
                    <button className="square">{this.props.value}</button>
                );
            }
        });

數字就被打上去了。


交互的組件

當點擊方塊時,打出“X”。

先把Square設置初始的state.value為null。當點擊小方框,觸發一個changeState方法。把當下的State改為X.

然后把渲染方法改為:

var Square=React.createClass({
    getInitialState:function(){
        return {
            value:null
        }
    },
    changeState:function(){
        this.setState({
            value:'X'
        })
    },
    render:function(){
        return (
            <button className="square" onClick={this.changeState}>{this.state.value}</button>
        );
    }
});

基本效果:

無論何時,this.setState只要被調用,組件將馬上更新並根據狀態渲染它的后代。


通過開發者工具看組件樹

插播一個廣告:React為開發者提供了適用於火狐及Chrome的擴展工具。有了它可以很方便看到你構建的組件庫。

當然Google商店現在得FQ才行。在安裝之后,勾選“允許訪問本地網址”,便可激活。


解除狀態

現在,井字棋已經有了個雛形。但是State被鎖定在每個單獨小的方塊中。

為了讓游戲能夠正常進行,還需要做一些事情:

  • 判斷勝負
  • XO的交替

為了判斷勝負,我們需要將9個方塊的value放到一塊。

你可能會想,為什么Board組件為什么不查詢每個組件的狀態並進行計算?——這在理論上是可行的。但是React不鼓勵這樣做——這樣導致代碼難讀,脆弱,變得難以重構。

相反,最好的解決方案就是把state放到Board組件上,而不是每個方塊里。Board組件可以告訴每個小方塊要顯示什么——通過之前加索引值的方法。

當你先從各種各樣的子代中把它們的數據統一起來,那就把state放到它們的父級組件上吧!然后通過props把數據全部傳下去。子組件就會根據這些props同步地展示內容。

在React里,組件做不下去的時候,把state向上放是很常見的處理辦法。正好借此機會來試一下:設置Board組件的狀態——為一個9個元素的數組(全部是null),以此對應九個方塊:

var Board=React.createClass({
    getInitialState:function(){
        return (
            squares:Array(9).fill(null),
        )
    },
  ...

到了后期,這個狀態可以指代一個棋局,比如這樣:

[
  'O', null, 'X',
  'X', 'X', 'O',
  'O', null, null,
]

然后把這個狀態數組分配到每個小方塊中(還記得renderSquare方法嗎?):

renderSquare:function(i){
    return   <Square value={this.state.squares[i]} />
},

再次把Square的組件改為{this.props.value}。現在需要改變點擊事件的方法。當點擊小方塊,通過回調props傳入到Square中,直接把Board組件state相應的值給改了:

return <Square value={this.state.squares[i]} onClick={() => this.handleClick(i)} />

這里的onClick不是一個事件。而是方塊組件的一個props。現在方塊組件Square接受到這個props方法,就把它綁定到真正的onClick上面:

<button className="square" onClick={() => this.props.onClick()}>{this.props.value}</button>

補白:ES6的箭頭函數

x => x * x

以上的意思是:

function (x) {
    return x * x;
}

箭頭函數相當於匿名函數,並且簡化了函數定義。

React在此引入箭頭函數處理的是this的問題。

如果不用箭頭函數寫是:

renderSquare:function(i){
    var _this=this;
    return   <Square onClick={function(){return _this.handleClick(i)}} value={this.state.squares[i]} />
},

選擇自己喜歡的就好。

現在根據就差定義面板組件中handleClick函數了。顯然點擊一下就刷新Board的狀態。以下兩種方法都可以。

handleClick:function(i){
    this.setState(function(prev){
        //console.log(prev.squares)
      	var arr=prev.squares;
        arr.squares[i]='X';
        return {
            squares:prev.arr
        };
    })
},
handleClick:function(i){
    var squares=this.state.squares.slice();
    squares[i]='X';
    this.setState({
        squares:squares
    })
},			

把狀態往上放,使得每個小方框不再擁有自己的狀態。面板組件會分配props給他們。只要狀態改變,下面的組件就會更新。


為什么不突變的數據很重要(Why Immutability Is Important)

在handleClick里面,用了一個slice()方法把原來的數組克隆出來。有效防止了數組被破壞。

“不突變的對象”這是一個重要的概念,值得React文檔重開一章來強調。

有兩種改變數據的辦法,一個是直接改變(突變,mutate),一種是存到一個變量里面。二者的結果是相同,但是后者有額外的好處。

跟蹤變化

查找一個突變對象(mutate)的數據變化是非常麻煩的。 這就要求比較當前對象之前的副本,還要遍歷整個對象樹,比較每個變量和價值。 這個過程變得越來越復雜。

而確定一個不突變的對象的數據變化相當容易。 如果這個對象和之前相比不同,那么數據就已改變了。就這么簡單。

決定React何時重新渲染

最大的好處:在構建簡單純粹的組件時, 因為不突變的數據可以更容易地確定是否更改了,也有助於確定一個組件是否需要被重新渲染。


功能組件

回到之前的項目,現在你不再需要Square組件中的構造函數了。 事實上,對於一個簡單而無狀態的功能性組件類型,比如Square,一個渲染方法足夠了,它只干一件事:根據上面傳下來的props來決定渲染什么,怎么渲染,完全沒必要再開一個擴展組件。

var Square=React.createClass({
    render:function(){
        return (
            <button className="square" onClick={() => this.props.onClick()}>{this.props.value}</button>
        );
    }
});

可以說這個square組件做到這里就完結了。不用再理他了。


決定次序

目前這個App最大的問題就是整個游戲竟然只有X玩家,簡直不能忍,還能不能好好的OOXX了?

對這個App來說誰先走肯定是狀態。這個狀態決定handleClick渲染的是X還是O:

首先,我們定義X玩家先走。

var Board=React.createClass({
    getInitialState:function(){
        return {
            squares:Array(9).fill(null),
            turnToX:true//為ture時輪到X走
        }
    },
  ...

每點擊一次,將造成這個開關的輪換。

handleClick:function(i){
    var squares=this.state.squares.slice();
    squares[i]=this.state.turnToX?'X':'O';
    this.setState({
        squares:squares,
        turnToX:!this.state.turnToX
    })
},

現在棋是走起來了。


判斷勝負

鑒於井字棋很簡單,獲勝的最終畫面只有8個。所以判斷勝負用窮舉法就可以了。也就是說,當squares數組出現8個情況,就宣告勝者並終止游戲。這里妨自己寫寫判斷勝負的引擎:

function judgeWinner(square){
    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[1]]&&squares[winCase[1]]==squares[winCase[2]]){//三子一線
            return squares(winCase[0]);//返回勝利一方的標識
        }
    }
    return false;
}

這個方法在Board渲染前執行就可以了。

...
render:function(){
    var winner=judgeWinner(this.state.squares);//每次渲染都判斷獲勝者
    var status='';
    if(winner!==null){
        status='獲勝方是:'+winner
    }else{
        var player=this.state.turnToX?'X':'O';
        status='輪到'+player+'走'
    }
    return (
        <div clasName="board">
            <div className="status">{status}</div>
      ...

好啦!現在已經把這游戲給做出來了。你可以在電腦上自己跟自己下井字棋,一個React新手,走到這一步已是winner。來看看效果吧~

什么,真要完了嗎?還有一半的內容。


儲存歷史步數

現在我們嘗試做一個歷史步數管理。方便你悔棋或復盤(井字棋還得復盤?!)

每走一步,就刷新一次狀態,那就把這個狀態存到一個數組對象(比如history)中。調用這個歷史對象的是Game組件,要做這一步,就得把狀態進一步往上放(滿滿的都是套路啊)。

在Game當中設置狀態也是一個大工程。但是基本上和在Board里寫狀態差不多。

  • 首先,用一個history狀態存放每一步生成的squares數組。turnToX也提到Game組件中。
  • 找出最新的狀態history[history.length-1]lastHistory
  • 在handleClick方法中添加落子判斷:勝負已分或是已經落子則不響應。
  • 在Game渲染函數中寫好status,然后放到指定位置。
  • 把handleClick函數傳到Board組件去!
var Game=React.createClass({
            getInitialState:function(){
                return {
                    history:[
                        {squares:Array(9).fill(null)}
                    ],
                    turnToX:true
                }
            },
            handleClick:function(i){//這里的i是棋盤的點位。
                var history=this.state.history;
                var lastHistory=history[history.length-1];
                var winner=judgeWinner(lastHistory.squares);
                var squares=lastHistory.squares.slice();

                if(winner||squares[i]){//如果勝負已分,或者該位置已經落子,則不會響應!
                    return false;
                }
                squares[i]=this.state.turnToX?'X':'O';//決定該位置是X還是O

                this.setState({
                    history:history.concat([{squares:squares}]),
                    turnToX:!this.state.turnToX
                });//然后把修改后的squares橋接到狀態中去
            },
            render:function(){
                var history=this.state.history;
                var lastHistory=history[history.length-1];
                var winner=judgeWinner(lastHistory.squares);

                var status='';
                if(winner){
                    status='獲勝方是'+winner;
                }else{
                    var player=this.state.turnToX?'X':'O';
                    status='輪到'+player+'走';
                }

                return (
                    <div className="game">
                        <Board lastHistory={lastHistory.squares} onClick={(i)=>this.handleClick(i)} />
                        <div className="info">
                            <div>{status}</div>
                            <ol></ol>
                        </div>
                    </div>
                );
            }
        });

那么Board組件里面的各種狀態完全不需要了,只保留render和renderSquare函數足矣。

var Board=React.createClass({
    renderSquare:function(i){
        return <Square value={this.props.lastHistory[i]} onClick={() => this.props.onClick(i)} />
    },
    render:function(){
        return (
            <div clasName="board">
                <div className="status"></div>
                <div className="board-row">
                    {this.renderSquare(0)}
                    {this.renderSquare(1)}
                    {this.renderSquare(2)}
                </div>
                <div className="board-row">
                    {this.renderSquare(3)}
                    {this.renderSquare(4)}
                    {this.renderSquare(5)}
                </div>
                <div className="board-row">
                    {this.renderSquare(6)}
                    {this.renderSquare(7)}
                    {this.renderSquare(8)}
                </div>
            </div>
        );
    }
});

展示歷史步數

在之前入門學習中已經有了深刻體會:渲染一串React元素,最好用的方法是數組。

恰好我們的history也是一個數組。而且Game的架構設計中還有一個ol——那么會做了吧?

				...
				var arr=[];
				var _this=this;
                history.map(function(step,move){
                    var content='';
                    if(move){
                        content='Move#'+move;
                    }else{
                        content='游戲開始~';
                    }
                    arr.push(<li key={move}><a onClick={()=>_this.jumpTo(move)} href="javascript:;">{content}</a></li>);
                });
				...

在這個a標記里,還加了個this.jumpToMove。當點擊之后j將把該索引值的舊狀態作為最后一個狀態。

好了,現在話分兩頭,插播一段關於Key值的論述。


論Key值的重要性

任何一個數組,都必須有key值。

當你渲染一串組件,React總是會把一些信息安置到每個單獨組件里頭去。比如你渲染一串涉及state的組件時,這個state是得存起來的。不管你如何實現你的組件。React都會在背后存一個參照。

你對這些組件增刪改查。React通過這些參照信息得知哪些數據需要發生變動。

...
<li>蘇三說:xxx</li>
<li>李四說:ooo</li>
..

如上你想修改li的內容,React無法判斷哪個li是蘇三的,哪個li是李四的。這時就要一個key值(字符串)。對於同輩元素,key是唯一的。

<li key="蘇三">蘇三說:xxx</li>
<li key="李四">李四說:OOO</li>

key值是React保留的一個特殊屬性,它擁有比ref更先進的特性。當創建一個元素,React直接把一個key值傳到被return的元素中去。盡管看起來也是props之一,但是this.props.key這樣的查詢是無效的。

重新渲染一串組件,React通過key來查找需要渲染的匹配元素。可以這么說,key被添加到數組,那這個組件就創建了;key被移除,組件就被銷毀。key就是每個組件的身份標志,在重新渲染的時候就可以保持狀態。倘若你改變一個組件的key,它將完全銷毀,並重新創建一個新的狀態。

因此:強制要求你插入到頁面的數組元素有key,如果你不方便插入,那么一定是你的設計出了問題。


來場說走就走的時間旅行

由於添加了悔棋這一設定,而悔棋是不可預測的。所以井字棋組件初始需要多一個狀態:stepNumber:0。另一方面,悔棋導致turnToX需要重新設定。

jumpTo:function(step){
                this.setState({
                    stepNumber:step,
                    turnToX:step%2?false:true
                })
            },

留意到this.state.stepNumber其實可以取代history.length-1——那就在render方法和handleClick方法中全部把它替換了。

最后一個問題還是出在handleClick,雖然可以回退,但是狀態最終不能實時更新。用history=history.slice(0,this.state.stepNumber+1);把它剪切一下就行了。

那么全部功能就完成了。嗯,應該是完成了。

var Game=React.createClass({
            getInitialState:function(){
                return {
                    history:[
                        {squares:Array(9).fill(null)}
                    ],
                    turnToX:true,
                    stepNumber:0
                }
            },
            handleClick:function(i){
                var history=this.state.history;
                history=history.slice(0,this.state.stepNumber+1);
                var lastHistory=history[this.state.stepNumber];
                var winner=judgeWinner(lastHistory.squares);
                var squares=lastHistory.squares.slice();

                if(winner||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
                });
                console.log(this.state.history)
            },
            jumpTo:function(step){
                this.setState({
                    stepNumber:step,
                    turnToX:step%2?false:true
                });
            },
            render:function(){
                var history=this.state.history;
                var lastHistory=history[this.state.stepNumber];
                var winner=judgeWinner(lastHistory.squares);

                var status='';
                if(winner){
                    status='獲勝方是'+winner;
                }else{
                    var player=this.state.turnToX?'X':'O';
                    status='輪到'+player+'走';
                }

                var arr=[];
                var _this=this;
                history.map(function(step,move){
                    var content='';
                    if(move){
                        content='Move#'+move;
                    }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>
                );
            }
        });

        var Board=React.createClass({
            renderSquare:function(i){
                return <Square value={this.props.lastHistory[i]} onClick={() => this.props.onClick(i)} />
            },
            render:function(){
                return (
                    <div clasName="board">
                        <div className="status"></div>
                        <div className="board-row">
                            {this.renderSquare(0)}
                            {this.renderSquare(1)}
                            {this.renderSquare(2)}
                        </div>
                        <div className="board-row">
                            {this.renderSquare(3)}
                            {this.renderSquare(4)}
                            {this.renderSquare(5)}
                        </div>
                        <div className="board-row">
                            {this.renderSquare(6)}
                            {this.renderSquare(7)}
                            {this.renderSquare(8)}
                        </div>
                    </div>
                );
            }
        });

        var Square=React.createClass({
            render:function(){
                return (
                    <button className="square" onClick={() => this.props.onClick()}>{this.props.value}</button>
                );
            }
        });

        ReactDOM.render(
            <Game />,
            document.getElementById('container')
        );
        /**********************************/
        function judgeWinner(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[1]]&&squares[winCase[1]]==squares[winCase[2]]){//三子一線
                    return squares[winCase[0]];//返回勝利一方的標識
                }
            }
            return null;
        }

效果如下:


結束的升華

到目前為止,實現了一個井字棋游戲,有了以下基本功能

  • 你可以自己跟自己玩井字過三關
  • 判斷誰贏了
  • 記錄棋局
  • 還允許悔棋

挺好,挺好。

但是,你還可以改進:

  • 通過(X,Y)來取代數字坐標
  • 對右方的被選中的當前記錄進行加粗顯示
  • 用兩個循環重寫Board組件,替代掉原來生硬的代碼結構
  • 對你的歷史記錄進行升降序排列
  • 高亮顯示獲勝的結果
  • 加個人工智能什么的。

這些內容本系列筆記的第2第3篇。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM