259 lines
7.1 KiB
Markdown
259 lines
7.1 KiB
Markdown
---
|
||
title: 案例演示
|
||
date: 2021-03-27 15:04:09
|
||
permalink: /pages/87146f/
|
||
categories:
|
||
- 《React》笔记
|
||
- 案例演示
|
||
tags:
|
||
- React
|
||
author:
|
||
name: xugaoyi
|
||
link: https://github.com/xugaoyi
|
||
---
|
||
|
||
# 案例演示
|
||
|
||
本节内容根据官方文档的 [教程](https://zh-hans.reactjs.org/tutorial/tutorial.html) 编写。
|
||
|
||
[在线demo](https://codepen.io/gaearon/pen/gWWZgR?editors=0010)
|
||
|
||
|
||
|
||
## 案例代码
|
||
|
||
```jsx
|
||
import React from 'react'
|
||
import ReactDOM from 'react-dom'
|
||
import './index.css'
|
||
|
||
// 正方形格子 (没有state时可以使用函数组件)
|
||
function Square (props) {
|
||
// return 返回一个React元素
|
||
return (
|
||
// <button> 是内置组件,onClick属性是内置组件内规定的点击方法
|
||
<button
|
||
className="square"
|
||
onClick={props.onClick}
|
||
>
|
||
{props.value}
|
||
</button>
|
||
)
|
||
}
|
||
|
||
// `棋盘`组件(格子集合)
|
||
class Board extends React.Component {
|
||
renderSquare (i) {
|
||
// 加小括号是防止自动添加分号而破坏结构
|
||
return (
|
||
// 向Square组件传递了两个参数 value 和 onClick
|
||
// 注意:这里的onClick是传参,而不是直接绑定点击事件
|
||
// 事件命名规范:on[Event]={ this.handle[Event] }
|
||
<Square
|
||
value={this.props.squares[i]}
|
||
onClick={() => this.props.onClick(i)}
|
||
/>
|
||
)
|
||
}
|
||
|
||
// render 方法描述你想在屏幕看到的内容
|
||
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) {
|
||
// JavaScript class 继承中的构造函数内必需以super开头
|
||
super(props)
|
||
// state 是当前组件私有的
|
||
this.state = {
|
||
history: [
|
||
{
|
||
squares: Array(9).fill(null)
|
||
}
|
||
],
|
||
stepNumber: 0, // 当前正在查看的历史记录
|
||
xIsNext: true,
|
||
}
|
||
}
|
||
|
||
// 处理格子点击(由子组件触发)
|
||
handleClick (i) {
|
||
// 这里的slice方法在`回到过去`时,会在副本中把`未来`历史记录丢弃掉,当setState时会抹去原来的`未来`记录。
|
||
const history = this.state.history.slice(0, this.state.stepNumber + 1);
|
||
const current = history[history.length - 1]
|
||
const squares = current.squares.slice() // slice() 取数组副本
|
||
|
||
if (calculateWinner(squares) || squares[i]) { // 当有获胜者 或 当前格子已落棋
|
||
return;
|
||
}
|
||
squares[i] = this.state.xIsNext ? 'X' : 'O'
|
||
this.setState({
|
||
// concat不会改变原数组,而push会改变原数组
|
||
history: history.concat([{
|
||
squares
|
||
}]),
|
||
stepNumber: history.length,
|
||
xIsNext: !this.state.xIsNext
|
||
})
|
||
}
|
||
|
||
// 跳转历史步骤
|
||
jumpTo (step) {
|
||
// 这里并未修改history
|
||
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)
|
||
|
||
// 历史步骤 (map方法把原数组映射为另一个数组)
|
||
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;
|
||
} else {
|
||
status = '下一步玩家: ' + (this.state.xIsNext ? 'X' : 'O');
|
||
}
|
||
|
||
// 注意:这里的onClick是传参,而不是直接绑定点击事件
|
||
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;
|
||
}
|
||
|
||
```
|
||
|
||
|
||
|
||
## 为什么不可变性在 React 中非常重要
|
||
|
||
案例中,在修改`state`时并非直接修改对象属性内的某个直,而是先使用`.slice()` 方法创建了数组的一个副本。然后在通过`setState()`方法,替换掉整个数组。
|
||
|
||
|
||
|
||
一般来说,有两种改变数据的方式。第一种方式是直接*修改*变量的值,第二种方式是使用新的一份数据替换旧数据。
|
||
|
||
### 直接修改数据
|
||
|
||
```js
|
||
var player = {score: 1, name: 'Jeff'};
|
||
player.score = 2;
|
||
// player 修改后的值为 {score: 2, name: 'Jeff'}
|
||
```
|
||
|
||
|
||
|
||
### 新数据替换旧数据
|
||
|
||
```js
|
||
var player = {score: 1, name: 'Jeff'};
|
||
|
||
var newPlayer = Object.assign({}, player, {score: 2});
|
||
// player 的值没有改变, 但是 newPlayer 的值是 {score: 2, name: 'Jeff'}
|
||
|
||
// 使用对象展开语法,就可以写成:
|
||
// var newPlayer = {...player, score: 2};
|
||
```
|
||
|
||
不直接修改(或改变底层数据)这种方式和前一种方式的结果是一样的,这种方式有以下几点好处:
|
||
|
||
|
||
|
||
### 简化复杂的功能
|
||
|
||
**不可变性使得复杂的特性更容易实现**。在后面的章节里,我们会实现一种叫做“时间旅行”的功能。“时间旅行”可以使我们回顾井字棋的历史步骤,并且可以“跳回”之前的步骤。这个功能并不是只有游戏才会用到——撤销和恢复功能在开发中是一个很常见的需求。**不直接在数据上修改可以让我们追溯并复用游戏的历史记录**。
|
||
|
||
|
||
|
||
### 跟踪数据的改变
|
||
|
||
如果直接修改数据,那么就很难跟踪到数据的改变。跟踪数据的改变需要可变对象可以与改变之前的版本进行对比,这样整个对象树都需要被遍历一次。
|
||
|
||
**跟踪不可变数据的变化相对来说就容易多了。**如果发现对象变成了一个新对象,那么我们就可以说对象发生改变了。
|
||
|
||
####
|
||
|
||
### 确定在 React 中何时重新渲染
|
||
|
||
不可变性最主要的优势在于它可以帮助我们在 React 中创建 *pure components*。我们可以很轻松的确定不可变数据是否发生了改变,从而确定何时对组件进行重新渲染。
|
||
|
||
查阅[性能优化](https://zh-hans.reactjs.org/docs/optimizing-performance.html#examples)章节,以了解更多有关 `shouldComponentUpdate()` 函数及如何构建 *pure components* 的内容。
|
||
|
||
### |