常见的访谈问题:TIC TAC TOE
#javascript #react #typescript #面试

您是否正在为您与React合作的职位进行技术面试做准备?我看到给受访者最常见的任务之一是:“构建一个简单的TIC TAC TOE游戏”

通常,您正在协调采访的个人将使您对技术面试中的期望有一个模糊的概述。如果他们提到实施一个简单的游戏,那么您就在正确的位置!

如果您只想查看最终结果,请前往此GitHub repository

为什么将TIC-TAC TOE用于技术访谈?

与大多数技术访谈一样,TIC TAC TOE挑战使面试官有机会看到一些事情:

  1. 您如何看待构建功能
  2. 您对React的知识深度
  3. 您的交流

在面试中,您被要求建造任何东西,而不仅仅是tic-tac-toe,必须了解面试官在您身边并期待协作会议。

如果您陷入困境,提出问题!不确定提示吗? 询问它!您表明自己是一个不怕寻求帮助的团队合作者,您会给面试官提供与您合作的完整图片。

除了个性和协作技巧外,此测试是一个很好的方法来查看个人是否理解以下内容:

  1. 国家管理
  2. 受控组件
  3. 上下文api
  4. CSS和JSX技能
  5. 基本的JavaScript技能

解决方案可能是什么样的

您可以通过许多不同的方式来构建此游戏。在这里,我将带您浏览我认为非常简单的解决方案,并使用对React有很好理解的模式。

本教程将假定您的技术评估需要您使用Typescript,但是这些概念转化为JavaScript。

起点

通常在与基于React的挑战的技术访谈中,您将从基本的React应用程序开始。可能在诸如CoderPadCodeSandbox的平台中。

起点通常包括:

  • 运行React项目的基本设置
  • 一个包含App.tsxmain.tsxsrc/文件夹
  • 可能会删除或忽略的其他一些文件

在大多数情况下,您不必担心启动文件,因为您的焦点将放在App.tsx和您添加的新文件中。

建造游戏板

要开始构建游戏,我们将构建一个充当游戏板的组件。这是最终将呈现出来自Tic Tac Toe网格的所有正方形。

为此

mkdir src/components
touch src/components/GameBoard.tsx

您可能会在在线编辑中,您必须在其中手动通过UI

手动创建这些编辑

GameBoard.tsx文件将导出一个function component作为其默认导出:

// src/components/GameBoard.tsx
import React from 'react'
const GameBoard: React.FC = () => {
  return <></>
}

export default GameBoard

此组件需要渲染3 x 3个正方形。我们将假设有一个根级App.css文件,我们可以为本教程提供全局样式。

App.css中,添加以下内容:

/* src/App.css */
.gameboard {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  border-radius: 10px;
  overflow: hidden;
}

这应该给我们一个类,将div的内容作为3 x 3网格。

// src/components/GameBoard.tsx
import React from 'react'
const GameBoard: React.FC = () => {
  return <div className="gameboard">
    {/* Render grid here! */}
  </div>
}

export default GameBoard

在这一点上,我们有一个GameBoard组件,可以在游戏板上渲染一些网格项目。接下来,我们将解决这个问题。

构建平方分量

要启动将为每个网格项目渲染的组件,请在src/components中创建一个名为Square.tsx的新文件:

// src/components/Square.tsx
import React from 'react'
const Square: React.FC = () => {
  return <></>
}

export default Square

此组件将渲染包含X或O的正方形。创建另一个类以样式的这些正方形:

/* src/App.css */
.square {
  width: 100px;
  height: 100px;
  border: 1px solid gray;
  background: #f1f1f1;
  display: flex;
  align-items: center;
  justify-content: center;
}

然后将其用于新组件:

// src/components/Square.tsx
import React from 'react'
const Square: React.FC = () => {
  return <div className="square"></div>
}

export default Square

在该div标签中,您将在其中渲染x或o,具体取决于播放器单击正方形。

要为其未来用法准备此组件,请定义一些属性,您可以将其传递以指定选择它的用户和单击处理程序函数:

// src/components/Square.tsx
import React from 'react'

type props = {
  user: string | null
  onClick: () => void
}

const Square: React.FC<props> = ({ user, onClick }) => {
  return <div className="square" onClick={onClick}>
    {user}
  </div>
}

export default Square

我们现在有一个游戏板,也可以在该板上渲染正方形。接下来我们需要一些结构来存储游戏数据并根据该数据渲染板。

在这一点上,我们实际上并没有渲染我们已建立的任何一个组件。那很快就会来了!

使用上下文API构建游戏状态

要处理本应用程序中的状态数据,您将使用React的Context API。此API为您提供了一种向应用程序提供全球状态管理的方法,使您可以轻松地跨组件共享数据。

要保持井井有条,上下文通常存储在自己的文件夹中。在src/中创建一个名为contexts的新文件夹,并在该新目录中的一个名为GameState.tsx的文件:

mkdir src/contexts
touch src/contexts/GameState.tsx

此文件是我们将创建上下文的地方。将其打开并从创建新上下文并导出它开始:

// src/contexts/GameState.tsx
import React, { createContext } from 'react'

export type GameState = {
  users: string[]
  activeUser: string
  selections: Array<string | null>
}

export const GameStateContext = createContext<GameState>({
  users: ["x", "y"],
  activeUser: null,
  selections: [],
})

在上面的摘要中,我们正在导入createContext函数,该功能使我们能够创建一个反应上下文。我们还创建了一种类型的GameState来定义此上下文公开的属性。目前,它包含:

  • users:一个包含两个玩家数据的数组
  • activeUser:转弯的用户
  • selections:每个单独网格项目的数据

在这一点上,我们有一个上下文,但是我们还需要导出一个使用provider
提供该上下文的组件

// src/contexts/GameState.tsx
import React, { createContext, useState } from 'react'
// ...
export const GameStateProvider: React.FC = ({ children }) => {
  const [activeUser, setActiveUser] = useState("x")
  const [selections, setSelections] = useState<Array<string | null>>(
    Array(9).fill(null)
  )

  return <GameStateContext.Provider value={{
    users: ["x", "y"],
    activeUser,
    selections,
  }}>
    {children}
  </GameStateContext.Provider>
}

请注意,useState是从react库中添加到导入的

此提供商组件将包装整个应用程序,使其全局访问其提供的数据。重要的是在这里注意:

  • children参数,其中包含儿童组件最终将包装。
  • useState Invications。我们正在用两种数据来初始化提供商的状态。 activeUser默认为"x"selections,其中包含一个带有9长度的空数组。

将添加到此文件中,但是现在让我们继续使用上下文来使用上下文,以便我们可以开始渲染游戏板。

使用游戏上下文

要使用上下文,我们将首先导入提供商组件并将整个应用程序包装在其中。

进入src/App.tsx,并通过导入并将JSX内容包装到组件中来使用:

// src/App.tsx
import './App.css'
import { GameStateProvider } from './contexts/GameState'

function App() {
  return (
    <GameStateProvider>
      {/* original contents */}
    </GameStateProvider>
  )
}

GameStateProvider组件中包含的任何内容都可以访问GameState上下文。

GameBoard组件将是此游戏的切入点。接下来导入并用该组件替换GameStateProvider内部的内容:

// src/App.tsx
import { GameStateProvider } from './contexts/GameState'
import GameBoard from './components/GameBoard'

function App() {
  return (
    <GameStateProvider>
      <GameBoard />
    </GameStateProvider>
  )
}

这将导致GameBoard在屏幕上渲染,尽管该组件当前没有渲染任何正方形。

渲染游戏网格

如果您回想一下GameState上下文初始化,您会记住我们用九个项目的数组初始化了状态,该项目代表板上的网格项目。为了渲染游戏的UI,我们将为每个网格项目提供一个正方形。

src/components/GameBoard.tsx中,使用useContext访问上下文的数据并为selections阵列中的每个项目渲染Square组件:

// src/components/GameBoard.tsx
import React, { useContext } from 'react'
import Square from './Square'
import { GameStateContext } from '../contexts/GameState'

const GameBoard: React.FC = () => {
  const { selections } = useContext(GameStateContext)

  return <div className="gameboard">
    {
      selections.map(
        (selection, i) => 
          <Square 
            key={i}
            user={selection} 
            onClick={null}
          />
      )
    }
  </Container>
}

export default GameBoard

对于数组中的每个项目,您现在正在渲染Square并将选择数据传递到该正方形。最终,这将保留单击该正方形的用户的名称。

如果我们看屏幕,我们将看到一个非常无组织的网格:

Common React Interview Question: Tic Tac Toe

让我们通过将游戏板包装在App.tsx中的新的div并在App.css中添加一些样式来解决此问题:

// src/App.tsx
// ... 

function App() {
  return (
    <GameStateProvider>
      <div className="container">
        <GameBoard/>
      </div>
    </GameStateProvider>
  )
}

export default App

/* src/App.css */
/* ... */
.container {
  display: flex;
  width: 100vw;
  height: 100vh;
  align-items: center;
  justify-content: center;
  background: white;
  font-family: roboto;
}

,通过这些调整,您现在应该看到一个格式的TIC TAC TOE GRID!

Common React Interview Question: Tic Tac Toe

我们现在正在渲染TIC TAC TOE网格,并可以访问全球状态。您有所有需要开始使此电网功能功能的作品!

使玩家能够选择正方形

要开始,我们需要添加一种方法,并使其可以通过全局状态访问,该全局状态在用户单击正方形时会突变selections数组。单击正方形也应将转弯切换到下一个球员。

src/contexts/GameContext.tsx中进行以下更改,以添加一个基于当前玩家选择正方形的函数,然后将转弯传递给下一个玩家:

// src/contexts/GameContext.tsx
import React, { createContext, useState } from 'react'

export type GameState = {
  users: string[]
  activeUser: string
  selections: Array<string | null>
  makeSelection: (squareId: number) => void // 👈🏻
}

export const GameStateContext = createContext<GameState>({
  users: ["x", "o"],
  activeUser: null,
  selections: [],
  makeSelection: null, // 👈🏻
})

export const GameStateProvider: React.FC = ({ children }) => {
  const [activeUser, setActiveUser] = useState("x")
  const [selections, setSelections] = useState<Array<string | null>>(
    Array(9).fill(null)
  )

  // Allows a user to make a selection 👇🏻
  const makeSelection = (squareId: number) => {
    // Update selections
    setSelections(selections => {
      selections[squareId] = activeUser
      return [...selections]
    })

    // Switch active user to the next user's turn
    setActiveUser(activeUser === 'x' ? 'o' : 'x')
  }

  return <GameStateContext.Provider value={{
    users: ["x", "o"],
    activeUser,
    selections,
    makeSelection // 👈🏻
  }}>
    {children}
  </GameStateContext.Provider>
}

请注意手指向的位置!这些是所做的更改。

这是一个大型代码,所以让我们看看发生了什么变化:

  1. GameState类型需要一个名为makeSelection的新属性来定义我们添加的功能
  2. GameStateContext需要对makeSelection的初始值
  3. 提供商定义了makeSelection的功能
  4. makeSelection函数已提供给提供商的value属性,将其暴露于使用该上下文的任何组件

makeSelection函数本身有两件事。它采用squareId,即selections数组中的索引,并使用它用当前播放器的名称来更新该索引的值。然后,它将activeUser设置为哪个玩家不仅会选择。

结果,此功能允许用户选择一个正方形并传递转弯。剩下的就是要使用它。进入GameBoard组件,然后将其传递给渲染的Square组件的onClick处理程序:

// src/components/GameBoard.tsx
// ...

const GameBoard: React.FC = () => {
  const { 
    selections, 
    makeSelection // 👈🏻
  } = useContext(GameStateContext)
  return <div className="gameboard">
    {
      selections.map(
        (selection, i) => 
          <Square 
            key={i}
            user={selection} 
            onClick={() => makeSelection(i)} // 👈🏻
          />
      )
    }
  </div>
}

export default GameBoard

现在,当单击网格上的正方形时,将显示当前播放器的符号:

Common React Interview Question: Tic Tac Toe

剩下的就是添加功能以检查获胜者并在找到获胜者后重置游戏。

检查获胜者

当玩家选择正方形时,函数应触发,以检查是否有胜利组合。

为此,我们将在GameContextProvider组件中使用useEffect。每当selections发生变化时,效果都会发射为获胜者触发支票。

进行以下更改以实现这一目标:

// src/contexts/GameState.tsx
import React, { createContext, useState, useEffect } from 'react'
// ...
export const GameStateProvider: React.FC = ({ children }) => {
  const [selections, setSelections] = useState<Array<string | null>>(
    Array(9).fill(null)
  )
  // ...
  // 👇🏻
  useEffect(() => {
    // checkForWinner()
  }, [selections])

  return <GameStateContext.Provider value={{
    users: ["x", "o"],
    activeUser,
    selections,
    makeSelection
  }}>
    {children}
  </GameStateContext.Provider>
}

记住要导入useEffect

每当选择网格项目(或更具体地说,每当selections变量更改时)都会运行useEffect回调的代码。

我们需要添加以处理赢家的方法是添加一个新的状态变量,并通过名为winner的提供商提供它。当我们检查获胜者并找到一个时,播放器将存储在该状态变量中。

添加以下内容:

// src/contexts/GameState.tsx
import React, { createContext, useState, useEffect } from 'react'

// ...

export type GameState = {
  users: string[]
  activeUser: string
  selections: Array<string | null>
  makeSelection: (squareId: number) => void
  winner: string | null // 👈🏻
}

export const GameStateProvider: React.FC = ({ children }) => {
  // ...
  const [winner, setWinner] = useState(null) // 👈🏻
  // ...

  return <GameStateContext.Provider value={{
    users: ["x", "o"],
    activeUser,
    selections,
    makeSelection,
    winner // 👈🏻
  }}>
    {children}
  </GameStateContext.Provider>
}

现在有一个状态可以跟踪是否选择了赢家,以及谁是谁。处理获胜者的最后一块是读取游戏板并找到三个匹配选择的行的实际功能。

添加以下函数并取消点击useEffect回调的内容:

// src/contexts/GameState.tsx
import React, { createContext, useState, useEffect } from 'react'

// ...

export const GameStateProvider: React.FC = ({ children }) => {
  // ...
  const checkForWinner = () => {
    const winningCombos = [
      [0,1,2],
      [3,4,5],
      [6,7,8],
      [0,3,6],
      [1,4,7],
      [2,5,8],
      [0,4,8],
      [6,4,2]
    ]

    winningCombos.forEach( combo => {
      const code = combo.reduce(
        (acc, curr) => `${acc}${selections[curr]}`, 
        ''
      )
      if ( ['xxx', 'yyy'].includes(code) ) 
        setWinner(code[0])
      }
    )
  }

  useEffect(() => {
    checkForWinner()
  }, [selections])

  return <GameStateContext.Provider value={{
    users: ["x", "o"],
    activeUser,
    selections,
    makeSelection,
    winner
  }}>
    {children}
  </GameStateContext.Provider>
}

当玩家赢得比赛时,winner状态变量将被更新以包含获胜播放器的符号。目前,没有迹象表明有赢家。让我们解决这个问题并结束游戏。

展示赢家并重置董事会

要显示获胜者,我们可以使用新的winner变量,该变量可通过上下文提供商访问。

GameBoard组件中,添加一个useEffect,该useEffect观察更改winner变量。当该变量更新并包含获胜者时,显示一个警报,表示哪个播放器赢了:

// src/components/GameBoard.tsx
// 👇🏻
import React, { useContext, useEffect } from 'react'
// ...

const GameBoard: React.FC = () => {
  // 👇🏻
  const { selections, makeSelection, winner } = useContext(GameStateContext)

  // 👇🏻
  useEffect(() => {
    if ( winner !== null ) {
      alert(`Player ${winner.toUpperCase()} won!`)
    }
  }, [winner])

  return <div className="gameboard">
    {/* ... */}
  </div>
}

export default GameBoard

不要忘记导入useEffect

如果玩家选择三个,您现在应该看到一个警报,表示用户赢了!

Common React Interview Question: Tic Tac Toe

一旦解散该警报,游戏板就不会改变。目前无法重新开始(除了刷新浏览器外)。

GameState上下文中添加并公开一个新功能,该函数将状态变量重置为其原始值:

// src/contexts/GameState.tsx
import React, { createContext, useEffect, useState } from 'react'

export type GameState = {
  // ...
  reset: () => void
}

export const GameStateContext = createContext<GameState>({
  // ...
  reset: null
})

export const GameStateProvider: React.FC = ({ children }) => {
  // ...

  const reset = () => {
    setSelections(Array(9).fill(null))
    setWinner(null)
    setActiveUser('x')
  }

  return <GameStateContext.Provider value={{
    // ...
    reset
  }}>
    {children}
  </GameStateContext.Provider>
}

此功能将完全重置游戏,应​​在GameBoard组件中的alert之后直接运行:

// src/components/GameBoard.tsx
import React, { useContext, useEffect } from 'react'

const GameBoard: React.FC = () => {
  // 👇🏻
  const { selections, makeSelection, winner, reset } = useContext(GameStateContext)

  useEffect(() => {
    if ( winner !== null ) {
      alert(`Player ${winner.toUpperCase()} won!`)
      // 👇🏻
      reset()
    }
  }, [winner])

  return <div className="gameboard">
    {/* ... */}
  </div>
}

export default GameBoard

现在,当您解散alert时,您应该看到游戏板已重置并准备好进行新游戏!

闭幕词

我真的很喜欢这项任务,因为乍一看似乎很简单,它使面试官可以看到您了解诸如:

的概念
  • 可重复使用的组件
  • 全球国家管理
  • jsx
  • 基本逻辑
  • ...等等!

如果您在上面的教程中遇到麻烦,也可以参考完整项目的GitHub repository

非常感谢您的阅读和好运采访!