初学者参与React教程的成果展示

由React初学者完成的React教程输出。

这是我在Qiita上的首次投稿!
我目前正在努力学习成为一名Web前端工程师,而在实习期间,我们使用Vue和TypeScript进行开发。但是,通过参加Supporters(公司名)的活动,我发现很多企业都在使用React。而且,在我想要工作的企业中,React也是主流技术,因此我开始学习React!
同时,我听说学习一门新语言通常从教程开始(其实并不一定),所以我学习了React教程!
我认为在编程学习中,输出是非常重要的,所以这次我打算输出React教程的内容!
我知道可能会有错误,所以请在评论中指正?‍♂️

React 是什么?

React是一种声明式、高效且灵活的JavaScript库,用于构建用户界面。React的文件扩展名可以是.jsx或js。

请确认启动代码的内容。

class Square extends React.Component {
  render() {
    return (
      <button className="square">
        {/* TODO */}
      </button>
    );
  }
}

class Board extends React.Component {
  renderSquare(i) {
    return <Square />;
  }

  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">
          <Board />
        </div>
        <div className="game-info">
          <div>{/* status */}</div>
          <ol>{/* TODO */}</ol>
        </div>
      </div>
    );
  }
}

// ========================================

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

该系统分为三个组件,并且具有Game > Board > Square的父子关系。

逐一确认

class Square extends React.Component {
  render() {
    return (
      <button className="square">
        {/* TODO */}
      </button>
    );
  }
}

在Square组件中,只渲染了一个button标签。

class Board extends React.Component {
  renderSquare(i) {
    return <Square />;
  }

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

在Board组件中定义了一个名为renderSquare的方法,该方法调用了Square组件,并在render方法中向renderSquare方法传递了一个数字作为参数。
此外,还使用const定义了一个玩家。

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

使用Game组件调用并渲染Board组件。

使用这个起始代码将显示如下所示

Image from Gyazo

这部分与Vue相似,所以很容易理解!!

通过Props传递数据

class Board extends React.Component {
  renderSquare(i) {
    return <Square value={i} />; // valueという名前で引数iをpropsでSquareコンポーネントにわたしている
  }
}
class Square extends React.Component {
  render() {
    return (
      <button className="square">
        {this.props.value} // this.props.valueでpropsを受け取る
      </button>
    );
  }
}
Image from Gyazo

Vue.js和Props的传递方式有点不同,但我并没有困惑!(直到这里为止)

制作互动组件

class Square extends React.Component {
 render() {
   return (
     <button className="square" onClick={() => console.log('click')}> // OnClickメソッドにconsole.log('click)という関数を渡して、buttonタグが押された時にコンソールにclickと表示されるようにする。
       {this.props.value}
     </button>
   );
 }
}

在教程中提到,使用箭头函数来定义onclick方法会很好,但为什么会很好我很在意,所以我进行了一些调查,并找到了以下的文章。
为什么在React中要使用箭头函数
阅读后发现,JavaScript的this与此有很大的关系,内容相当深奥,因此我打算另外总结。
这次按照教程上所述,为了避免this的混乱行为,我会使用箭头函数。

然后使用 state 在 Square 组件中,让它记住自己被点击了,并显示一个“X”来填充方格。

class Square extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      value: null,
    };
  }
  省略

constructor是什么?super(props)是什么?这样的疑问出现了。
查阅后发现constructor与生命周期密切相关。

生命周期

参考1:
参考2:

生命周期是指React组件从创建到成长,最终死亡的一系列过程。

这就是组件的人生呢,哈哈。

组件生成 => 组件被创建并渲染的过程 => 挂载
组件更新 => 用户更新组件管理的数据的过程 => 更新
组件销毁 => 组件变得不再需要并被丢弃的过程 => 卸载
Image from Gyazo

在使用Vue进行开发时,我并没有太意识到生命周期,所以对其理解遇到了困难。

接下来,修改Square的render方法,使其在被点击时显示当前state的值。

class Square extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      value: null,
    };
  }

  render() {
    return (
      <button
        className="square"
        onClick={() => this.setState({value: 'X'})} // setStateを使うことでstateの値をXに変更する
      >
        {this.state.value} // stateを表示する
      </button>
    );
  }
}

我之前听说React在状态管理方面很难,但是我感觉我可能会有一些困难理解(但是也期待着?)。

国家的提升

目前的代碼中,Square組件保持了狀態,但是與其由每個Square組件來保持遊戲狀態,不如由父級的Board組件來保持,這樣父級組件可以通過props將信息傳遞給子組件,同時也使子組件與其他兄弟或父級之間實現了常時同步。

class Board extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      squares: Array(9).fill(null), //初期 state として 9 個のマス目に対応する 9 個の null 値をセット
    };
  }
 省略
renderSquare(i) {
    return <Square value={this.state.squares[i]} />; // Squareがvalueプロパティ('X'、'O'、または空のマス目の場合は null)を受け取るようになる
  }

由于Board组件是保存状态的,所以需要从Square组件更改状态。
但是,不能直接从Square直接更改Board的状态。
相反,可以将一个函数从Board传递给Square,这样当方格被点击时,让Square调用该函数。

renderSquare(i) {
    return (
      <Square
        value={this.state.squares[i]}
        onClick={() => this.handleClick(i)} //handleClickメソッドをSquareに渡す
      />
    );
  }
class Square extends React.Component {
// constructorを削除
  render() {
    return (
      <button
        className="square"
        onClick={() => this.props.onClick()} // this.props.onClickでBoardのhandleClickをコールする 
      >
        {this.props.value} // this.props.valueでBoardのstate.squaresを受け取る
      </button>
    );
  }
}

由于未定义handleClick,因此需要定义它。

handleClick(i) {
    const squares = this.state.squares.slice(); //sliceメソッドでstate.squaresのコピーを作る
    squares[i] = 'X';
    this.setState({squares: squares});
  }

为了避免直接更改squares,使用.slice()方法创建了一个数组副本。这似乎与不可变性相关。

为什么不可变性很重要?

一般而言,對於變動的資料有兩種不同的方法。第一種方法是直接修改資料的值,對資料進行變異(mutate;改寫)。第二種方法是創建一份經過所需修改的新資料拷貝,並替換原有的舊資料。

带有变异的数据改变

var player = {score: 1, name: 'Jeff'};
player.score = 2;
// Now player is {score: 2, name: 'Jeff'}

不涉及变异的数据变化

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

var newPlayer = Object.assign({}, player, {score: 2});
// Now player is unchanged, but newPlayer is {score: 2, name: 'Jeff'}

// Or if you are using object spread syntax proposal, you can write:
// var newPlayer = {...player, score: 2};

使用Object.assign将player的值复制给newPlayer,并通过第三个参数更新score的值。

最终的结果一样,但通过不直接对数据进行改变(即不改写内部数据),可以获得以下一些优点。

・能夠輕鬆實現複雜功能
・檢測變更
・決定React的重新渲染時間

在教程中写着这样,但是我没有太明白,所以我进行了一些调查,并找到了一篇很好的文章!
重要的是不可变性的原因,以及如何使用Immer来轻松实现!
我将大致总结一下这篇文章中的内容。

1. “不变性”是指什么?

就像之前提到的,不可变性指的是状态不会被修改的意思。
在编程中,不可变性意味着不改变状态。

2. 不可变性的重要原因

2-1. 可以阻止无法预料的状态更新。

状态是显示应用程序当前情况的东西。然而,如果在各个地方频繁引用或更改状态,可能会导致无法确定应用程序的当前状态的危险。

因为举例很容易理解,所以我将进行介绍。

const hi = { greeting: 'おはよう' };

// コード1万行

console.log(`${hi.greeting}, みんちゃん`);

如果在这里看到控制台窗口,早上好,可能会出现Minchan。但是,如果有人在一万行代码中更改了hi对象,突然就会出现Goodbye和Minchan等可能性也完全存在。在这种情况下,很难找到是谁更新了hi对象。(而且这是一个错误,所以也不会出现错误)。

如果代码量只是在教程或个人开发时的水平,那么更新状态也会很容易确认。但如果项目规模很大,代码量庞大,找到state的变更也很困难。

2-2. 可以追踪state的变化

当更新对象时,如果生成一个新的”更新后对象”,那么在比较更新前后的对象时,结果会为false。借助这一事实,可以知道对象已经被更新。
这也是React为什么重视不可变性的原因。React会检测状态的变化,并在判断变化前后的对象不同时重新渲染组件。在进行比较时,如果内存地址不同,则结果为false,因此可以快速判断。

虽然我对此还不是很了解,但React是通过虚拟DOM的机制工作的,因此React会检测到构建在其上的虚拟DOM的变化,并将其反映到HTML中。因此,能否追踪state的变化对于React的性能非常重要。

3. 不变性真麻烦

在教程中学习时,我觉得很烦恼,所以当在这篇文章中也被介绍为烦恼时,我松了一口气笑了出来。
听说有一个叫做immer的库,它可以帮助我们轻松地编写遵守不可变性的代码,以解决这种烦人问题,我也想学习关于这个!

我觉得在React中,不可变性非常重要,所以我想要好好学习!学习React时,我深刻意识到我对JavaScript的理解非常浅?

函数组件

有一篇关于函数组件的文章,它是以React教程中的函数组件为基础编写的。我将参考该文章并将其整理成另一篇文章。

处理轮次

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();
    squares[i] = this.state.xIsNext ? 'X' : 'O'; // xIsNextがtrueなら✖️を表示、falseなら◯を表示
    this.setState({
      squares: squares,
      xIsNext: !this.state.xIsNext, // ✖️か○を表示させたらxIsNextを反転させて手番が変わる
    });
  }
render() {
    const status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O'); // 手番が誰の番なのかをxIsNextを使って表示

    return (
      // the rest has not changed

游戏胜利者的决定

定义一个判断胜者的助手方法。

function calculateWinner(squares) {
  const lines = [ // この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]; // [a,b,c]にlinesの配列を入れていく
    if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) { 
      return squares[a]; // 横、縦、斜めに同じマークで埋められていないかを判断して埋まっていた場合、そのマークをreturnする
    }
  }
  return null;
}

在这个方法方面,由于教程几乎没有解释,所以我非常难理解。

render() {
    const winner = calculateWinner(this.state.squares); // winnerには○か✖️かnullが入る
    let status;
    if (winner) { 
      status = 'Winner: ' + winner; // winnerがnull以外の時にwinnerを返す
    } else {
      status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O'); // winnerがnullの時は次の番をプレイヤーを返す
    }
 handleClick(i) {
    const squares = this.state.squares.slice();
    if (calculateWinner(squares) || squares[i]) { // 勝者が決まっているかマス目が既に埋まっている場合はreturnを返す
      return;
    }
    squares[i] = this.state.xIsNext ? 'X' : 'O';
    this.setState({
      squares: squares,
      xIsNext: !this.state.xIsNext,
    });
  }

现在我们可以玩完全的井字游戏了!

添加时光旅行功能

保存着手历史

将过去的 squares 数组保存到名为 history 的另一个数组中。这个 history 数组表示了从开始到结束的棋盘的所有状态,并具有以下结构。

history = [
  // Before first move
  {
    squares: [
      null, null, null,
      null, null, null,
      null, null, null,
    ]
  },
  // After first move
  {
    squares: [
      null, null, null,
      null, 'X', null,
      null, null, null,
    ]
  },
  // After second move
  {
    squares: [
      null, null, null,
      null, 'X', null,
      null, null, 'O',
    ]
  },
  // ...
]

国家的提升,再次发生。

通过将history state放置在Game组件中,可以从子组件Board中移除squares的state。与将Square组件中的“state提升”并移动到Board组件中完全相同,现在我们将把Board中的state提升到顶级的Game组件中。通过这样做,Game组件将完全控制Board的数据,并能够在history中渲染Board的先前移动的数据。

class Game extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      history: [{
        squares: Array(9).fill(null),
      }],
      xIsNext: true,
    };
  }

接下来,我们将使Board组件从Game组件中接收squares和onClick属性。由于Board内只有一个点击事件处理程序与多个方块对应,我们将把方块的位置传递给onClick处理程序,以告知哪个方块被点击。按照以下步骤重写Board组件:
• 删除Board的constructor。
• 将renderSquare中的this.state.squares[i]替换为this.props.squares[i]。
• 将renderSquare中的this.handleClick(i)替换为this.props.onClick(i)。

class Board extends React.Component {
  // constructorを削除
  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
        value={this.props.squares[i]} // Gameからpropsとして受け取るため変更
        onClick={() => this.props.onClick(i)} // Gameからpropsとして受け取るため変更
      />
    );
  }
以下略

更新游戏组件的render函数来确保在决定和显示游戏状态文本时使用最新的历史记录。

render() {
    const history = this.state.history;
    const current = history[history.length - 1];
    const winner = calculateWinner(current.squares);
    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>{/* TODO */}</ol>
        </div>
      </div>
    );
  }

当我看到这段代码时

const history = this.state.history;
const current = history[history.length - 1];

当我无法完全理解这部分内容时,经过查看handliClick方法后,我明白了。

handleClick(i) {
    const history = this.state.history;
    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,
      }]),
      xIsNext: !this.state.xIsNext,
    });
  }

以下的部分非常重要

history: history.concat([{
        squares: squares,
      }]),

在这部分中,通过使用setState方法来更新history,然后通过使用concat方法将新的squares添加到history中(这也是不可变性生效的部分)。
通过这样做,可以创建出如上述所示的history。

history = [
  // Before first move
  {
    squares: [
      null, null, null,
      null, null, null,
      null, null, null,
    ]
  },
  // After first move
  {
    squares: [
      null, null, null,
      null, 'X', null,
      null, null, null,
    ]
  },
  // After second move
  {
    squares: [
      null, null, null,
      null, 'X', null,
      null, null, 'O',
    ]
  },
  // ...
]

我能理解刚刚这段代码的意思

const history = this.state.history;
const current = history[history.length - 1];

将history的状态放入state,将history的方块放入current。
之所以要使用history[history.length – 1],是因为要获取当前state的方块,需要取得history数组的最后一个元素。
例如,如果当前的history如下:

history = [
  // 0回目
  {
    squares: [
      null, null, null,
      null, null, null,
      null, null, null,
    ]
  },
  // 1回目
  {
    squares: [
      null, null, null,
      null, 'X', null,
      null, null, null,
    ]
  },
  // 2回目(現在)
  {
    squares: [
      null, null, null,
      null, 'X', null,
      null, null, 'O',
    ]
  },

在数组中,squares的索引号是2。
要获取它,可以通过history.length来确认数组的总数。在例子中,可以得到3。
然后减去1,就可以得到最后一个数组项!!

过去的行动显示

const moves = history.map((step, move) => { // stepには要素、moveにはindex番号が入る
      const desc = move ? // moveがあるかを判別
        'Go to move #' + move :
        'Go to game start';
      return (
        <li>
          <button onClick={() => this.jumpTo(move)}>{desc}</button> 
        </li>
      );
    });

选择钥匙。

当React重新渲染列表时,它会检查每个列表项目的key,以确定前一个列表项中是否存在相同的key。如果新列表中包含以前没有的key,React会创建一个新组件。如果前一个列表中的key在新列表中不存在,React会销毁前一个组件。如果两个key匹配,则相应的组件会被移动。key向React提供有关每个组件的唯一性的信息,从而使React能够在重新渲染之间保持状态。如果组件的key发生变化,组件将被销毁并使用新的状态重新创建。

key是一个特殊的属性,由React保留(ref也是类似的高级功能)。在创建元素时,React会提取出key属性,并直接将该键存储在返回的元素中。key看起来像是props的一部分,但不能通过this.props.key进行引用。React会自动使用key来确定哪些子元素应该被更新。组件无法自行检查其key的方法。

在构建动态列表时,强烈推荐分配正确的key。如果没有合适的key,可能需要重新构建数据结构以确保存在此类key。

如果未指定key,React将显示警告并默认使用数组索引作为key。将数组索引用作key会导致在排序、插入/删除项目时出现问题。可以通过显式地传递key={i}来消除警告,但这样做会导致与使用数组索引相同的问题,因此大多数情况下不建议这样做。

key不需要在全局范围内是唯一的,只要在组件和其兄弟之间是唯一的即可。

因此我做了一些研究,因為又遇到了一個困難哈哈。
關於列表和鍵(key)
正如教程中所述,鍵的作用是將重繪過程中的繪製處理浪費降到最低,以防止性能下降。
這裡有一個很好的例子被介紹了。

<ul>
         + <li>lion</li>
        <li>cat</li>
        <li>dog</li>
        <li>bird</li>
</ul>

当有这样的列表时,尝试添加“lion”。

猫是第一位,狗是第二位,鸟是第三位,而狮子是第零位。即使只添加了一个数据,但DOM的兄弟元素数据却全部发生了改变。这意味着React应该重新渲染的目标范围扩大了。

给先前表示的过去操作的按钮列表也设置键。

const moves = history.map((step, move) => {
      const desc = move ?
        'Go to move #' + move :
        'Go to game start';
      return (
        <li key={move}> // keyを設定
          <button onClick={() => this.jumpTo(move)}>{desc}</button>
        </li>
      );
    });

这也意味着关键点与React的性能有关。对于我这个从未考虑过性能的人来说,这些方面理解起来非常困难。?

时间旅行的实现

class Game extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      history: [{
        squares: Array(9).fill(null),
      }],
      stepNumber: 0, //stateとしてstepNumberを追加
      xIsNext: true,
    };
  }
 jumpTo(step) {  // 過去の着手を表示するメソッド
    this.setState({
      stepNumber: step, 
      xIsNext: (step % 2) === 0, // stepが偶数の時は○の番なのでxIsNextをtrueにする
    });
  }
handleClick(i) {
    const history = this.state.history.slice(0, this.state.stepNumber + 1); //過去の着手に戻った際に戻る前の将来の履歴を消すためにsliceを使っている
    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, // stepNumberを更新する
      xIsNext: !this.state.xIsNext,
    });
  }
render() {
    const history = this.state.history;
    const current = history[this.state.stepNumber]; // stepNumberによって現在選択されている着手をレンダーする。
    const winner = calculateWinner(current.squares);

总结

我之前听说React很难,但没想到这么难?
不过只学习了教程,我觉得大概能理解React,并且学习过程非常有趣!
我意识到自己在JavaScript理解和性能方面的知识还有很大的不足。
我也想学习Redux和Hooks(应该也会很难吧〜)
这次写的React教程代码

bannerAds