使用 Vue.js 改寫 React 的官方教程井字棋


React 的官方教程井字棋很好的引導初學者一步步走進 React 的世界,我想類似的教程對 Vue.js 的初學者應該也會有啟發,於是使用 Vue.js 進行了改寫

可以先查看最終的結果,嘗試點擊體驗,我們將逐步地實現這個效果

初始狀態代碼

初始狀態查看

打開初始狀態直接編輯,或者將對應的文件復制下來放置在同一文件夾中
此時只是一個簡單的井字棋格子,以及寫死的下一個選手

初始代碼分析

目前定義了三個組件,分別為 Square,Board 和 Game

Square 目前只是一個普通的按鈕

Vue.component('Square', {
  template: `
    <button class="square">
      {{ /* TODO */ }}
    </button>
  `
})
  • 這樣定義了組件后,別的組件就可以直接以 <Square /> 的方式引用該組件

Board 模版由當前狀態和 9 個 Square 組成

Vue.component('Board', {
  data() {
    return {
      status: `${nextLabel}X`,
      board: [
        [0, 1, 2],
        [3, 4, 5],
        [6, 7, 8]
      ]
    }
  },
  template: `
    <div>
      <div class="status">{{ status }}</div>
      <div class="board-row" v-for="(row, index) in board" :key="index">
        <Square v-for="square in row" :key="square" />
      </div>
    </div>
  `
});
  • data 定義了當前狀態 status,和 board 的值,這樣在模版中就可以用 {{ status }} 的方式引用狀態值,使用 v-for 將 board 二維數組里的值兩次循環組裝成井字格
  • 在組件中的 data 必須是返回對象的函數而非對象字面值
  • v-for 需要有 key 確保性能以及不報警告

Game 模版由 Board 與 后面需要增加的狀態和歷史組成

Vue.component('Game', {
  template: `
    <div class="game">
      <div class="game-board">
        <Board />
      </div>
      <div class="game-info">
        <div>{{ /* status */ }}</div>
        <ol>{{ /* TODO */ }}</ol>
      </div>
    </div>
  `
});

增加數據處理

增加 Props

在 Board 中傳遞一個名為 value 的 prop 到 Square

<Square v-for="square in row" :key="square" :value="square" />
  • :value 是 v-bind:value 的縮寫,表示其值是一個表達式

在 Square 的組件定義和模版中增加 value prop

Vue.component('Square', {
  props: ['value'],
  template: `
    <button class="square">
      {{ value }}
    </button>
  `
})
  • props 為父組件可傳遞給子組件的變量,在父組件調用子組件時在標簽中設置對應屬性,在子組件中使用方法與 data 一致

目前的代碼和效果:0 - 8 的數字分別填充進井字棋格中

增加交互

增加點擊事件至按鈕元素以更新值

Vue.component('Square', {
  //props: ['value'],
  data() {
    return {
      value: null
    }
  },
  methods: {
    setValue() {
      this.value = 'X';
    }
  },
  template: `
    <button class="square" @click="setValue">
      {{ value }}
    </button>
  `
})
  • @click 為 v-on:click 的縮寫,其值為點擊需要運行的函數,這里為組件定義的方法 methods 中的 setValue
  • 子組件不能直接更新父組件的值,所以將 value 從 props 改為 data
  • data 的值更新,對應模版就會自動更新展示內容

目前的代碼和效果:點擊井字棋格,對應填充 X

完善游戲

數值提升

為交替落子和確認輸贏,需要統一判斷各格狀態,所以將 value 提升至 Board

Board 增加數據 squares 和方法 handleClick

Vue.component('Board', {
  data() {
    return {
      ...
      squares: Array(9).fill(null),
    }
  },
  methods: {
    handleClick(i) {
      const squares = this.squares.slice();
      if (squares[i]){
        alert('此位置已被占!');
        return
      }
      squares[i] = 'X';
      this.squares = squares;
    }
  },
  template: `
    ...
      <div class="board-row" v-for="(row, index) in board" :key="index">
        <Square v-for="square in row" :key="square" :value="squares[square]" @click="handleClick(square)" />
  • squares 初始為 9 個 null 組成的數組,井字棋盤為空的狀態
  • handleClick 接收對應格子序號的參數,並更新對應的 square 元素
  • 事件處理器不是 handleClick(square) 的返回值,而是 handleClick,只是在觸發時會帶上參數值 square

在 Square 的點擊事件處理器中觸發 Board 的點擊事件

Vue.component('Square', {
  props: ['value'],
  methods: {
    setValue() {
      this.$emit('click');
    }
  },
  • value 要從 data 改回到 props
  • $emit 可以調用父組件傳遞的事件處理器
  • prop 里的值在父組件更新,子組件模版也會對應更新展示內容

目前的代碼和效果:點擊井字棋格,如果未被占,則填充 X

輪流落子

增加數據 xIsNext,並在點擊時切換

data() {
  return {
    ...
    xIsNext: true
  }
},
methods: {
    handleClick(i) {
      ...
      squares[i] = this.xIsNext ? 'X' : 'O';
      this.squares = squares;
      this.xIsNext = !this.xIsNext;
      this.status = `${nextLabel}${this.xIsNext ? 'X' : 'O'}`;
  • xIsNext 初始值為 true,即 X 先落子
  • 點擊后,通過取反交替 xIsNext
  • 更新狀態值 status 為下一個落子者

目前的代碼和效果:點擊井字棋格,X 和 O 交替落子

判斷勝者

增加計算勝者的函數

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;
}
  • 列舉可能獲勝的組合,與 squares 數組的值進行比對

增加點擊處理函數的勝者邏輯

if (calculateWinner(squares)) {
  alert('勝負已定!');
  return;
}
...
const winner = calculateWinner(squares);
if (winner) {
  this.status = '獲勝者: ' + winner;
  return;
}
  • 點擊后,如果之前已有取勝,則點擊無效
  • 處理落子后,再次判斷是否取勝,更新狀態

目前的代碼和效果:有一方獲勝后, 狀態和點擊處理更新

增加時間旅行

保存歷史記錄

為實現“悔棋”功能,需要記錄每一次落子的整體狀態,相當於棋盤的快照,作為一個歷史記錄,提升至 Game 組件中

在 Game 增加數據 history,將 xIsNext,status 和 handleClick 方法 從 Board 中轉移到 Game 中

Vue.component('Game', {
  data() {
    return {
      history: [{
        squares: Array(9).fill(null),
      }],
      xIsNext: true,
      status: `${nextLabel}X`
    }
  },
  methods: {
    handleClick(i) {
      const history = this.history;
      const current = history[history.length - 1]
      const squares = current.squares.slice();
      ...
      squares[i] = this.xIsNext ? 'X' : 'O';
      history.push({
        squares: squares
      });
      ...
    }
  },
  template: `
    <div class="game">
      <div class="game-board">
        <Board :squares="history[history.length - 1].squares" @click="handleClick" />
  `
})
  • squares 從 history 的最后一個記錄取值(目前只有一個記錄)
  • 落子后,squares 把落子記錄進去后,history 再增加一個記錄

Board 增加 prop squares,handleClick 更新為調用父組件的事件處理器

Vue.component('Board', {
  props: ['squares'],
  methods: {
    handleClick(i) {
      this.$emit('click', i);
    }
  },

目前的代碼和效果:狀態位置更新,歷史記錄已存儲

展示歷史步驟記錄

把歷史記錄循環展示出來,並綁定點擊事件,通過 stepNumber 的更新顯示對應步驟的記錄

Vue.component('Game', {
  data() {
    ...
      stepNumber: 0,
    ...
    }
  },
  methods: {
    handleClick(i) {
      const history = this.history.slice(0, this.stepNumber + 1);
      ...
      this.history = history.concat([{
        squares: squares
      }]);
      this.stepNumber = history.length;
      ...
    },
    jumpTo(step) {
      if(step === this.stepNumber){
        alert('已在' + (0 === step ? '最開始' : `步驟#${step}!`));
        return;
      }
      this.stepNumber = step;
      this.xIsNext = (step % 2) === 0;
      this.status = `${nextLabel}${this.xIsNext ? 'X' : 'O'}`;
    }
  },
  template: `
    <div class="game">
      <div class="game-board">
        <Board :squares="history[this.stepNumber].squares" @click="handleClick" />
      </div>
      <div class="game-info">
        <div>{{ status }}</div>
        <ol>
          <li v-for="(squares, index) in history" :key="index" :class="{'move-on': index === stepNumber}">
            <button @click="jumpTo(index)">{{ 0 === index ? '回到開始' : '回到步驟#' + index }}</button>
   ...
  `
})
  • 在 Game 中增加 stepNumber,初始為 0,記錄當前展示的步驟
  • 將 Board 的 prop squares 的取值更新為 this.stepNumber 對應的步驟
  • handleClick 中以已當前步驟為基礎處理 history,並更新 stepNumber
  • 增加方法 jumpTo 處理回到歷史的展示,更新 stepNumber,xIsNext 和 status

最終的代碼和效果:每落一子,都會增加一個歷史步驟,點擊步驟可回到該步

總結

游戲實現內容

  • 交替落子
  • 判斷輸贏
  • 悔棋重來

展示技術內容

  • v-bind:在模版中進行數據綁定
  • v-for:在模版中進行數組循環
  • v-on, $emit:在組件間進行事件傳遞和觸發
  • data:在組件的定義和模版自動更新
  • prop:在組件間的傳遞和模版自動更新


免責聲明!

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



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