目录

案例演示

# 案例演示

本节内容根据官方文档的 教程 (opens new window) 编写。

在线demo (opens new window)

# 案例代码

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;
}

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

# 为什么不可变性在 React 中非常重要

案例中,在修改state时并非直接修改对象属性内的某个直,而是先使用.slice() 方法创建了数组的一个副本。然后在通过setState()方法,替换掉整个数组。

一般来说,有两种改变数据的方式。第一种方式是直接修改变量的值,第二种方式是使用新的一份数据替换旧数据。

# 直接修改数据

var player = {score: 1, name: 'Jeff'};
player.score = 2;
// player 修改后的值为 {score: 2, name: 'Jeff'}
1
2
3

# 新数据替换旧数据

var player = {score: 1, name: 'Jeff'};

var newPlayer = Object.assign({}, player, {score: 2});
// player 的值没有改变, 但是 newPlayer 的值是 {score: 2, name: 'Jeff'}

// 使用对象展开语法,就可以写成:
// var newPlayer = {...player, score: 2};
1
2
3
4
5
6
7

不直接修改(或改变底层数据)这种方式和前一种方式的结果是一样的,这种方式有以下几点好处:

# 简化复杂的功能

不可变性使得复杂的特性更容易实现。在后面的章节里,我们会实现一种叫做“时间旅行”的功能。“时间旅行”可以使我们回顾井字棋的历史步骤,并且可以“跳回”之前的步骤。这个功能并不是只有游戏才会用到——撤销和恢复功能在开发中是一个很常见的需求。不直接在数据上修改可以让我们追溯并复用游戏的历史记录

# 跟踪数据的改变

如果直接修改数据,那么就很难跟踪到数据的改变。跟踪数据的改变需要可变对象可以与改变之前的版本进行对比,这样整个对象树都需要被遍历一次。

**跟踪不可变数据的变化相对来说就容易多了。**如果发现对象变成了一个新对象,那么我们就可以说对象发生改变了。

#

# 确定在 React 中何时重新渲染

不可变性最主要的优势在于它可以帮助我们在 React 中创建 pure components。我们可以很轻松的确定不可变数据是否发生了改变,从而确定何时对组件进行重新渲染。

查阅性能优化 (opens new window)章节,以了解更多有关 shouldComponentUpdate() 函数及如何构建 pure components 的内容。

#

上次更新: 2023/07/11, 15:43:07
最近更新
01
基本知识
07-18
02
卷积神经网络识别图像
07-18
03
损失函数
07-18
更多文章>