对React官方教程 入门教程: 认识 React 的总结。
graph TB;
React[ReactDOM] --包含--> Game[Game class Component] --包含--> Board[Board class Component] --包含--> Square[Square class/function Component]
Game调用Board,传入props.squares和props.onClick(this.handleClick)。
return ( // ... <Board squares={current.squares} onClick={i => this.handleClick(i)} /> // ... )
Board调用Square,传入props.value和props.onClick(Game.handleClick)。
renderSquare(i) { return ( <Square // 这里就传入了props.value和props.onClick这两个参数 value={this.state.squares[i]} // onClick={() => this.handleClick(i)} // 状态还在 Board 的时候,传入Square的props.onClick就是Board.handleClick(i)。 onClick={() => this.props.onClick(i)} // 状态不在 Board 的时候,传入Square的props.onClick变成this.props.onClick(i),也就是Game.handleClick(i)。 /> ); }
Square返回一个Button,设置了props.value为button显示的值,设置props.onClick为button的点击事件触发函数。
function Square(props) { return ( <button className="square" onClick={props.onClick} > {props.value} </button> ); }
可以发现,状态(数据)从Game先传到Board,再从Board传递到Square。而触发事件绑定从Square上传到Board,再从Board上传到Game。最终游戏判定和信息更新的相关逻辑都在Game组件中得以实现,而数据展示只在下层被控组件中完成。
第一阶段:展示效果
这个阶段,主要完成基础九宫格展示和点击事件响应(无论如何都设置为“X”)。
组件作用
ReactDOM 负责调用 Game 组件
Game组件
Game.render() 负责调用 Board 组件
Board组件
Board.constructor() 负责初始化状态(属性)this.state.squares。
Board.renderSquare() 负责渲染一个Square组件。
Board.render() 负责调用九次Board.renderSquare()渲染出一个完整的九宫格。
Square组件
Square.constructor() 负责初始化this.state.value。
Square.render() 负责渲染一个Button作为九宫格的一个小格子。
核心代码
index.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 class Square extends React.Component { // TODO: remove the constructor constructor(props) { super(props); this.state = { value: null, }; } render() { // TODO: use onClick={this.props.onClick} // TODO: replace this.state.value with this.props.value return ( <button className="square" onClick={() => this.setState({value: 'X'})}> {/* Square最终渲染一个button,并绑定其点击事件为return外层Square的setState()。 */} {this.state.value} </button> ); } } class Board extends React.Component { constructor(props) { super(props); this.state = { squares: Array(9).fill(null), }; } renderSquare(i) { {/* Board返回Square组件 */} return <Square value={this.state.squares[i]} />; } render() { const status = 'Next player: X'; return ( <div> <div className="status">{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> ); } } class Game extends React.Component { render() { return ( <div className="game"> <div className="game-board"> {/* Game返回Board组件 */} <Board /> </div> <div className="game-info"> <div>{/* status */}</div> <ol>{/* TODO */}</ol> </div> </div> ); } } // ======================================== ReactDOM.render( // ReactDOM返回Game组件 <Game />, document.getElementById('root') );
主要效果
image-20200623162737674
在线代码
第二阶段: 完成基本功能
这一阶段,将状态从Square组件提升到了Board组件(状态提升 )。Square 组件不再持有 state,因此每次它们被点击的时候,Square 组件就会从 Board 组件中接收值,并且通知 Board 组件。
实现了游戏中“X”和“O”的交替出现,完成了游戏胜负的判定。
组件作用
index.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 function Square(props) { return ( <button className="square" onClick={props.onClick} > {props.value} </button> ); } class Board extends React.Component { constructor(props) { super(props); this.state = { squares: Array(9).fill(null), xIsNext: true, }; } handleClick(i) { const squares = this.state.squares.slice(); if (calculateWinner(squares) || squares[i]) { return; } squares[i] = this.state.xIsNext ? 'X' : 'O'; this.setState({ squares: squares, xIsNext: !this.state.xIsNext, }); } renderSquare(i) { return ( <Square // 这里就传入了props.value和props.onClick这两个参数 value={this.state.squares[i]} onClick={() => this.handleClick(i)} /> ); } render() { const winner = calculateWinner(this.state.squares); let status; if (winner) { status = 'Winner: ' + winner; } else { status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O'); } return ( <div> <div className="status">{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> ); } } class Game extends React.Component { render() { return ( <div className="game"> <div className="game-board"> <Board /> </div> <div className="game-info"> <div>{/* status */}</div> <ol>{/* TODO */}</ol> </div> </div> ); } } // ======================================== ReactDOM.render( <Game />, document.getElementById('root') ); function calculateWinner(squares) { const lines = [ [0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6], [1, 4, 7], [2, 5, 8], [0, 4, 8], [2, 4, 6], ]; for (let i = 0; i < lines.length; i++) { const [a, b, c] = lines[i]; if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) { return squares[a]; } } return null; }
主要效果
image-20200623170141198
在线代码
第三阶段:添加回溯功能
基于数据不变性,添加了 history
变量,再次进行状态提升,将状态提升到Game组件中,Board 中不再保留 constructor、handleClick等函数,只保留renderSquare和render来向下传递数据。
组件作用
核心代码
index.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 function Square(props) { return ( <button className="square" onClick={props.onClick}> {props.value} </button> ); } class Board extends React.Component { renderSquare(i) { return ( <Square value={this.props.squares[i]} onClick={() => this.props.onClick(i)} /> ); } render() { return ( <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> ); } } class Game extends React.Component { constructor(props) { super(props); this.state = { history: [ { squares: Array(9).fill(null) } ], stepNumber: 0, xIsNext: true }; } handleClick(i) { const history = this.state.history.slice(0, this.state.stepNumber + 1); const current = history[history.length - 1]; const squares = current.squares.slice(); if (calculateWinner(squares) || squares[i]) { return; } squares[i] = this.state.xIsNext ? "X" : "O"; this.setState({ history: history.concat([ { squares: squares } ]), stepNumber: history.length, xIsNext: !this.state.xIsNext }); } jumpTo(step) { this.setState({ stepNumber: step, xIsNext: (step % 2) === 0 }); } render() { const history = this.state.history; const current = history[this.state.stepNumber]; const winner = calculateWinner(current.squares); const moves = history.map((step, move) => { const desc = move ? 'Go to move #' + move : 'Go to game start'; return ( <li key={move}> <button onClick={() => this.jumpTo(move)}>{desc}</button> </li> ); }); let status; if (winner) { status = "Winner: " + winner; } else { status = "Next player: " + (this.state.xIsNext ? "X" : "O"); } return ( <div className="game"> <div className="game-board"> <Board squares={current.squares} onClick={i => this.handleClick(i)} /> </div> <div className="game-info"> <div>{status}</div> <ol>{moves}</ol> </div> </div> ); } } // ======================================== ReactDOM.render(<Game />, document.getElementById("root")); function calculateWinner(squares) { const lines = [ [0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6], [1, 4, 7], [2, 5, 8], [0, 4, 8], [2, 4, 6] ]; for (let i = 0; i < lines.length; i++) { const [a, b, c] = lines[i]; if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) { return squares[a]; } } return null; }
主要效果
image-20200623175403690
在线代码
关键点回顾
这个history变量要放到哪里?
我们希望顶层 Game 组件展示出一个历史步骤的列表。这个功能需要访问 history
的数据,因此我们把 history
这个 state 放在顶层 Game 组件中。
我们把 history
state 放在了 Game 组件中,这样就可以从它的子组件 Board 里面删除掉 square
中的 state。把 state 从 Board 组件提升到顶层的 Game 组件里。这样,Game 组件就拥有了对 Board 组件数据的完全控制权,除此之外,还可以让 Game 组件控制 Board 组件,并根据 history
渲染历史步骤。
下一步,我们让 Board 组件从 Game 组件中接收 squares
和 onClick
这两个 props。
因为当前在 Board 组件中已经有一个对 Square 点击事件的监听函数了,所以我们需要把每一个 Square 的对应位置传递给 onClick
监听函数,这样监听函数就知道具体哪一个 Square 被点击了。
状态从 Board组件 提升到 Game组件,需要完成以下三个步骤
不在Board组件中初始化状态了,删除 Board 组件中的 constructor
构造函数。
Board组件renderSquare
函数中对状态的调用都从 this.state.squares[i]
变为 this.props.squares[i]
,从此,状态都是从 Game组件传递过来的了。
在Board组件中,将状态传递给Game组件,把 Board 组件的 renderSquare
中的 this.handleClick(i)
替换为 this.props.onClick(i)
。
养成好习惯,用箭头函数来替代this。
// 箭头函数 <button className="square" onClick={() => alert('click') }> // this函数 <button className="square" onClick={function() {alert('click')}}>
React构造函数
在 JavaScript class 中,每次你定义其子类的构造函数时,都需要调用 super
方法。因此,在所有含有构造函数的的 React 组件中,构造函数必须以 super(props)
开头。
constructor(props) { super(props); this.state = { value: null, }; }
renturn的格式
为了提高可读性,我们把返回的 React 元素拆分成了多行,同时在最外层加了小括号,这样 JavaScript 解析的时候就不会在 return
的后面自动插入一个分号从而破坏代码结构了。
return ( <Square value={this.state.squares[i]} onClick={() => this.handleClick(i)} /> );
事件绑定
React的事件绑定和普通JS的事件绑定有所不同
// JS Style <button onclick="activateLasers()"> Activate Lasers </button> // React Style <button onClick={activateLasers}> Activate Lasers </button>
handleClick()
handleClick
是React中默认的点击事件组件响应的方法。这种方式的好处是:每次render方法的调用,不会重新创建一个新的事件响应函数,没有额外的性能损失。但是,使用这种方式要在构造函数中为作为事件响应的方法(handleClick),手动绑定this: this.handleClick = this.handleClick.bind(this)
,这是因为ES6 语法的缘故,ES6 Class 的方法默认不会把this绑定到当前的实例对象上,需要我们手动绑定。
handleClick(i) { const squares = this.state.squares.slice(); squares[i] = 'X'; this.setState({squares: squares}); }
数据的不可变性
修改数据有两种方式
在原来的数据上修改
复制之后修改
var player = {score: 1, name: 'Jeff'}; var newPlayer = Object.assign({}, player, {score: 2}); // player 的值没有改变, 但是 newPlayer 的值是 {score: 2, name: 'Jeff'} // 使用对象展开语法,就可以写成: var newnewPlayer = {...player, score: 5}; // player 的值没有改变, 但是 newnewPlayer 的值是 {score: 5, name: 'Jeff'}
第二种方法有以下几种好处
保持数据的不可变性质,不可变性使得复杂的特性更容易实现 。撤销和恢复功能在开发中是一个很常见的需求。保持了数据的不变性能够很容易的实现这些需求。
易于跟踪数据的改变 ,发现有新的对象复制,就可以判定发生了数据改变。
由于数据的变化十分易于跟踪,因此可以很轻松的确定不可变数据是否发生了改变,从而确定渲染时机,即何时对组件进行重新渲染。
在js中,使用slice()
方法返回一个新的数组对象,这一对象是一个由 begin
和 end
决定的原数组的浅拷贝 (包括 begin
,不包括end
)。slice
不会修改原数组,只会返回一个浅复制了原数组中的元素的一个新数组。拷贝原则是,如果要拷贝的是可变对象,就拷贝其引用。否则是不可变对象,拷贝的就是其本身。
从 class组件 到 function组件
如果你想写的组件只包含一个 render
方法,并且不包含 state,那么使用函数组件就会更简单。 我们不需要定义一个继承于 React.Component
的类,我们可以定义一个函数,这个函数接收 props
作为参数,然后返回需要渲染的元素。函数组件写起来并不像 class 组件那么繁琐,很多组件都可以使用函数组件来写。
class组件
class Square extends React.Component { render() { return ( <button className="square" onClick={() => this.props.onClick()} > {this.props.value} </button> ); } }
function组件
function Square(props) { // 把两个 this.props 都替换成了 props。 return ( <button className="square" {% 函数绑定也变得更简单了 PS: 这里绑定的是onClick不是onClick() %} onClick={props.onClick} > {props.value} </button> ) };
JS三段表达式
var a = true; undefined a ? "X" : "O" "X" var a = false; undefined a ? "X" : "O" "O"
React组件渲染原理
React 元素被视为 JavaScript 一等公民中的对象(first-class JavaScript objects)。因此我们可以把 React 元素在应用程序中当作参数来传递。在 React 中,我们还可以使用 React 元素的数组来渲染多个元素。
每当一个列表重新渲染时,React 会根据每一项列表元素的 key 来检索上一次渲染时与每个 key 所匹配的列表项。如果 React 发现当前的列表有一个之前不存在的 key,那么就会创建出一个新的组件。如果 React 发现和之前对比少了一个 key,那么就会销毁之前对应的组件。如果一个组件的 key 发生了变化,这个组件会被销毁,然后使用新的 state 重新创建一份。
我们强烈推荐,每次只要你构建动态列表的时候,都要指定一个合适的 key。
如果你没有指定任何 key,React 会发出警告,并且会把数组的索引当作默认的 key。但是如果想要对列表进行重新排序、新增、删除操作时,把数组索引作为 key 是有问题的。显式地使用 key={i}
来指定 key 确实会消除警告,但是仍然和数组索引存在同样的问题,所以大多数情况下最好不要这么做。
组件的 key 值并不需要在全局都保证唯一,只需要在当前的同一级元素之前保证唯一即可。
Code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 import React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; import { mockComponent } from 'react-dom/test-utils'; // 如果你想写的组件只包含一个 render 方法,并且不包含 state,那么使用函数组件就会更简单。我们不需要定义一个继承于 React.Component 的类,我们可以定义一个函数,这个函数接收 props 作为参数,然后返回需要渲染的元素。 // class Square extends React.Component { // // 我们用 state 来实现所谓“记忆”的功能。 // // constructor函数会初始化this.state // // this.state 中存储当前每个方格(Square)的值,并且在每次方格被点击的时候改变这个值。 // // 删掉 Square 组件中的构造函数 constructor,因为该组件不需要再保存游戏的 state。数据被放到Board中了,Square就只是起到一个接收点击事件触发的作用。 // // constructor(props) { // // super(props); // // this.state = { // // value: null, // // }; // // } // render() { // // Square组件渲染了一个单独的<button> // // 在 Square 组件 render 方法中的 onClick 事件监听函数中调用 this.setState, // // 我们就可以在每次 <button> 被点击的时候通知 React 去重新渲染 Square 组件。 // return ( // <button // className="square" // onClick={() => this.props.onClick()} // > // {/* {this.states.value} */} // {/* 不再使用this.states.value进行数据绑定了,直接进行用props进行数据绑定。 */} // {this.props.value} // </button> // ); // } // } function Square(props) { // 把两个 this.props 都替换成了 props。 return ( <button className="square" onClick={props.onClick} // 函数绑定也变得更简单了 这里绑定的是onClick不是onClick() > {props.value} </button> ) }; // Board组件渲染了9个方块。 class Board extends React.Component { // version 1.0 状态提升之后,this.state就不在 Board 中做处理了。 // constructor(props) { // super(props); // this.state = { // // 在构造函数中新建了一个dict,保存下棋结果,其中this.state.squares是一个list。 // squares: Array(9).fill(null), // xIsNext: true, // 构造函数中的初始 state 来设置默认的第一步棋子为 “X” // }; // } // 在状态由 Board 提升到 Game 之后,Board组件不在负责处理 Square 的点击事件响应。 // 而是交由上一级组件 Game组件 来处理。 // handleClick是点击事件的默认响应的方法 // square触发的点击事件,就是由handleClick来处理的。 // Board.handleClick组件中都是调用本组件的state。 // 所以用的都是this.setState。 // handleClick(i) { // // 调用 slice() 方法创建了 squares 数组的一个副本,而不是直接在现有的数组上进行修改。 // const squares = this.state.squares.slice(); // // 当有玩家胜出时,或者某个 Square 已经被填充时,该函数不做任何处理直接返回。 // if (calculateWinner(squares) || squares[i]) { // return; // } // // squares[i] 不再固定,“X”和“O”依次切换,棋子每移动一步,xIsNext(布尔值)都会反转,该值将确定下一步轮到哪个玩家,并且游戏的状态会被保存下来。 // squares[i] = this.state.xIsNext ? 'X' : 'O'; // // squares[i] = 'X'; // this.setState({ // squares: squares, // // 每次操作,都翻转一次xIsNext的值。 // xIsNext: !this.state.xIsNext, // }); // } renderSquare(i) { // 传递一个名为 value 的 prop 渲染到 Square 当中 // 把一个 prop 从父组件 Board “传递”给了子组件 Square。 // return <Square value={i}/>; // 这种方式是默认填写输入数字 i 的 return ( <Square value={this.props.squares[i]} // 这里绑定的事件从handleClick变成onClick // Board不再负责处理点击事件了,而是向上将点击事件传递到Game组件。 onClick={() => this.props.onClick(i)} /> ); } render() { // 在状态由 Board 提升到 Game 之后,游戏状态更新由Game负责,这一部分代码就可以注释掉了。 // 在渲染之前先做一次胜负判定 更新游戏状态 // const winner = calculateWinner(this.state.squares); // let status; // if(winner) { // status = "Winner: " + winner; // } else { // status = "Next player: " + (this.state.xIsNext ? "X" : "0"); // } // 开始渲染 return ( // return的是一个完整的div <div> {/* 在状态由 Board 提升到 Game 之后,游戏状态更新由Game负责,这一行展示代码就可以注释掉了。 */} {/* <div className="status">{status}</div> */} <div className="board-row"> {/* 调用renderSquare函数渲染 */} {/* 里面的数字i锚定在renderSquare函数中要更新了this.state.squares[]中的第i个元素。 */} {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> ); } } // Game组件渲染了含有默认值的一个棋盘 // Game组件调用了<Board />组件 class Game extends React.Component { // 将状态state从this.state从Board提升到Game。 constructor(props) { super(props); this.state = { history: [{ squares: Array(9).fill(null), }], xIsNext: true, // X先行 stepNumber: 0, // 记录当前查看哪一行 }; } // Square组件触发的点击事件,被层层向上传递,最终由Game组件的handleClick函数来处理。 handleClick(i) { // 更新 State 状态。 // 调用 slice() 方法创建了 squares 数组的一个副本,而不是直接在现有的数组上进行修改。 const history = this.state.history.slice(0, this.state.stepNumber + 1); const current = history[history.length - 1]; const squares = current.squares.slice(); // 更新游戏状态 // 当有玩家胜出时,或者某个 Square 已经被填充时,该函数不做任何处理直接返回。 if (calculateWinner(squares) || squares[i]) { return; } // squares[i] // 固定要填入的字符 // 不再固定要填入的字符,“X”和“O”依次切换,棋子每移动一步,xIsNext(布尔值)都会反转, // 该值将确定下一步轮到哪个玩家,并且游戏的状态会被保存下来。 squares[i] = this.state.xIsNext ? 'X' : 'O'; // squares中保存的就是要填入的数据。 this.setState({ // concat() 方法与 push() 方法不太一样,它并不会改变原数组,所以推荐使用 concat()。 history: history.concat([{ squares: squares, }]), // 每次操作都更新stepNumber stepNumber: history.length, // 每次操作,都翻转一次xIsNext的值。 xIsNext: !this.state.xIsNext, }); } jumpTo(step) { this.setState ({ stepNumber: step, xIsNext: (step % 2) === 0, }); } // 更新 Game 组件的 render 函数, // 使用最新一次历史记录来确定并展示游戏的状态。 render() { // 保存游戏数据 const history = this.state.history; // 最后,修改 Game 组件的 render 方法,将代码从始终根据 // 最后一次移动渲染修改为根据当前 stepNumber 渲染。 // const current = history[history.length-1]; const current = history[this.state.stepNumber]; // 更新游戏跳转步骤 const moves = history.map((step, move) => { const desc = move ? "Go to move #" + move : "Go to game Start" return ( <li key={move}> <button onClick={() => this.jumpTo(move)}> {desc} </button> </li> ); }); // 更新游戏状态 let status; const winner = calculateWinner(current.squares); if (winner) { status = "Winner: " + winner; } else { status = "Next player: " + (this.state.xIsNext ? "X" : "0"); } // 开始渲染 return ( <div className="game"> <div className="game-board"> <Board squares={current.squares} onClick={(i) => this.handleClick(i)} /> </div> <div className="game-info"> {/* 展示游戏状态 */} <div>{status}</div> <ol>{moves}</ol> </div> </div> ); } } // ======================================== ReactDOM.render( <Game />, document.getElementById('root') ); // 传入长度为 9 的数组,此函数将判断出获胜者,并根据情况返回 “X”,“O” 或 “null”。 function calculateWinner(squares) { const lines = [ [0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6], [1, 4, 7], [2, 5, 8], [0, 4, 8], [2, 4, 6], ]; for (let i=0; i<lines.length; i++) { const [a, b, c] = lines[i]; if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) { return squares[a]; } } return null; }