iT邦幫忙

2019 iT 邦幫忙鐵人賽

DAY 5
1
Modern Web

以經典小遊戲為主題之ReactJS應用練習系列 第 5

Day05 - Tic Tac Toe篇:事件處理

  • 分享至 

  • xImage
  •  

點擊方格 下棋事件

經過了 Day04,我們已經把棋盤畫出來了,接下來我們迫不及待地很想要來下棋。所以我們所要做的事情,就是我們要告訴我們的程式我們到底點了哪個方格,然後下的棋是圈圈還是叉叉。記得 Day03 裡面我們有提到,blocks 這個參數儲存棋盤上每一個格子當下的狀態,其中每一個格子有他專屬的 id,以及紀錄是圈圈還是叉叉佔有這個格子。

當我們能夠知道所點擊的格子的 id,並且知道當前是誰在下棋,把它記錄在 blocks 裡面,就可以了。可以實現的方法有很多種,在這邊我簡單分享我的方法。

取得方格 ID

首先我們需要讓每一個 block 被點擊的時候觸發onClick事件,觸發之後我要執行我所指定的函數,這邊我命名為 handleOnClick ,然後每一個block我會給他一個 data-* attribute 的屬性值,這邊我命名為data-id,顧名思義,我在裡面放了 block 的專屬 id,這樣我就可以在 handleOnClick 被執行的時候,透過 getAttribute() 這個方法順利地取得被點擊 block 的 id 了。

實作方法如下
handleOnClick

實作成果如下,點擊哪個位置,我們就印出那個位置的 id
handleOnClick-demo

值得一提的是,如果 onClick 觸發的函數 handleOnClick 不是一個箭頭函數的話,記得要在 constructor 裡面綁定 this。理由是因為當使用 extend React.Component 的方式去宣告元件的時候, React 雖然也會綁定 this 到元件內,但是只有在生命週期中(ex: componentDidMount)和 render 內,其他自己定義的 property 就不會被綁入 this ,而且 this 會被指到 windows 這個全域上。而箭頭函數當中的 this 是定義時的對象,不是使用時的對象,所以在箭頭函數中,this 指稱的對象在所定義時就固定了,而不會隨著使用時的脈絡而改變。因此在這邊 handleOnClick 如果是一個箭頭函數,他就不用在 constructor 裡面綁定 this。

React 與 bind this 的一些心得
[筆記] JavaScript ES6 中的箭頭函數(arrow function)及對 this 的影響

更新棋盤資料

取得 id 之後,就可以發一個 action 到 reducer 去更新 state 的資料了。
set-block-value

這邊我們 action 的 type 是 SET_BLOCK_VALUE ,在裡面做的事情有兩件,一個是把當前是圈圈還是叉叉存進指定 id 的 block 裡面,另一件事情是要換人下棋,這邊換人的方法是如同我 Day03 對 currentRole 這個參數的說明,我用1代表圈圈,用-1代表叉叉,因為我希望乘以-1就可以達到換人的效果,而這邊的TOGGLE就是-1

做好之後,這邊簡單 demo 一下,剛剛我是把 block 的 id 直接 show 在方格裡,我稍微改一下程式,讓 block 裡面 show 的是 owner。
set-block-value-demo

備註

特別要注意的是,如果方格被重複點擊,需要防止原本已經有 owner 的 block 不能再被新的 owner 覆蓋掉,所以這邊要記得做檢查。

改變棋盤大小事件

為了改變棋盤大小,我們需要一個 Selection 選單,然後因為獲勝條件我也想設計成一個 Selection 選單,功能是一樣的,所以我想要把他元件化,讓這兩個選單可以共用同一個元件模組,達到不要重複造輪子的精神。

首先,在 components 資料夾下面,我新增一個 Selection 元件
add-selection-component

這個元件有兩個輸入,一個是 options,一個是 handleOnSelect。 options 是一個 array of number ,因為我們的棋盤大小 gameScale 參數以及獲勝條件 winnerCondition 參數都是數字。

用來選擇 gameScale 的 options 中,最小的數字我定為 3,也就是原本的圈圈叉叉遊戲。最大的話我目前暫定 20,因為再大上去我覺得格子會太小。而用來選擇 winnerCondition 的 options 中,最小的數字一樣我設為 3,最大的數字則不能大於目前的 gameScale,因為如果 winnerCondition 超過棋盤的最大行數或是列數,就永遠都沒有贏家。

handleOnSelect 是我想要用來取得選到的 option 的函數。

根據上述的說明,首先我們要來產生一個數字是 3~20 的陣列

[3, 4, 5, 6, 7, 8, …, 18, 19, 20]

要做到這點,我提供幾個方法跟大家分享

  1. 使用for迴圈,把資料一個一個循序push進去一個array
  2. 透過 Array.from() 方法
  3. 使用lodash

lodash 介紹

這邊來介紹一下lodash,lodash是一個擴充與增加 javscript 功能的 library ,我要使用裡面提供的range()方法,可以幫助我們的很簡潔的產生我們想要的陣列,如下:

import _ from 'lodash';
const gameScaleOptions = _.range(DEFAULT_NUM_OF_BLOCK, 20 + 1);

用同樣的方法,我們也可以來產生 winnerConditionOptions

const winnerConditionOptions = _.range(DEFAULT_WINNER_CONDITION, gameScale + 1);

充分利用 lodash 讓你的程式碼更易讀及維護

reselect 介紹

這邊我們再介紹一個package,reselect,reselect是一個根據redux的state來計算衍生資料的函式庫,並且這個函式庫是當衍生資料依賴的state發生了變化,才會被重新計算,不然就繼續用原來的。換句話說,這個 library 有快取、記憶的作用。

以上述的例子來說,我們的 winnerConditionOptions 可以說是根據 gameScale 來產生的,隨著 gameScale 的變化,winnerConditionOptions 會重新計算並產生新的陣列,當然我們也可以在 redux 的 state 裡面存一筆 winnerConditionOptions 的資料,但是這麼做就會有點重複存資料的味道,因為 winnerConditionOptions 是從 gameScale 產生出來的。

又或者,我們可以在 TicTacToe 的 index.js 裡面,或是在 reducer 或 mapStateToProps 這些地方,寫一個計算的函式,拿到 gameScale 之後,再計算出 winnerConditionOptions,但是這個缺點在於,只要每次 state 產生變化,就會導致計算執行一次,這樣就會導致很多無用的計算。如果計算不復雜,效能上的確沒多大的區別,但反之,就會造成效能上的不足。而 reselect 幫我們做好了記憶快取的工作。即使 state 變化了,但是衍生資料的依賴的 state 中的資料沒有發生變化,計算是不會被執行的。

reselect - npm
reselect原始碼解讀

所以在這種狀況下,我們就可以把計算 winnerConditionOptions 的程式碼用 reselect 來處理,之後再把計算好的資料以 props 傳進 Selection 裡面。
makeSelectWinnerConditionOptions
add-selection-component-to-view

然後跟前面一開始一樣,我們可以透過 handleOnClick 拿到 block 的 id,同樣的方法,我們也可以在點擊 select 的 option 的時候,拿到我們想要的 gameScale 以及 winnerCondition。
handleOnSelect

並且我們的也在選取的時候發一個 action 到 reducer,來修改我們state裡面的值
set-game-scale

備註

在更新 gameScale 的時候我們要特別留意,因為我們希望 gameScale 改變的時候,棋盤上的格子布局也會跟著改變,而且我們希望 gameScale 改變之後,遊戲可以重來,所以更新完 gameScale 的時候,我們可以再做一次 setInit(),如此就會產生出新的 blocks,否則就會出現我們 gameScale 已經從3變成5了,但是 blocks 的總數還會是只有9,所以我們就會看到佈局已經變成5x5的佈局,但是畫面仍然只有9個格子的狀況,因為 blocks 並沒有被更新的關係。

Restart事件

今天我們要講的最後一個事件,是Restart Button被點擊之後觸發的重設遊戲事件。這邊做法很簡單,點擊的時候發送一個action到reducer,然後把所有參數重設為初始值 setInit(),就搞定了!

<button
    className="tic-tac-toe__restart-btn"
    onClick={this.handleOnRestartClick}
>
    Restart
</button>

handleOnRestartGame

今日成果

做到這邊,我們今天的工作就完成了!下面demo一下今天辛苦的成果!包含:

  1. block被點擊之後,能夠下棋的事件
  2. Scale Selection改變gameScale,讓棋盤伸縮自如的事件
  3. Condition Selection改變winnerCondition,改變獲勝條件事件
  4. 按下Restart Button重新開始遊戲事件

event-handle-final-demo

Tic Tac Toe - Github


上一篇
Day04 - Tic Tac Toe篇:頁面佈局規劃及棋盤實現
下一篇
Day06 - Tic Tac Toe篇:自己刻圈圈元件及叉叉元件
系列文
以經典小遊戲為主題之ReactJS應用練習30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言