2023 Vue 開發者的 React 入門

VueReact 都是流行的 JavaScript 框架,它們在組件化、數據綁定等方面有很多相似之處

本文默認已有現代前端開發 (Vue) 背景,關於 組件化、前端路由、狀態管理 概念不會過多介紹

0 基礎建議詳細閱讀 Thinking in React - 官方文檔 瞭解 React 的設計哲學

經過本文的學習讓沒開發過 React 項目的 Vue 開發者可以上手開發現有的 React 項目,完成工作需求開發

React 新文檔

React 新文檔重新設計了導航結構,讓我們更加輕鬆地找到所需的文檔和示例代碼 不僅提供了基礎知識的介紹,還提供了更加詳細的原理介紹和最佳實踐,包括:React 組件的設計哲學、React Hooks的原理和用法等

並且提供了在線編輯和運行的功能,方便開發者進行測試和實驗

👇 基於 函數組件

初學可以只學 函數組件,You Don't Need to Learn Class Components

👇 interactive sandboxes 可交互沙箱,邊做邊學

Fork 可以單獨打開頁籤

JSX 與 SFC

雖然 ReactVue 在組件定義方式上存在差異,但是它們的組件化思想是相似的

根節點

👇 Vue

<template>
  <div>同級節點1</div>
  <div>同級節點2</div>
</template>

👇 React

const App = (
  <>
    <div>同級節點1</div>
    <div>同級節點2</div>
  </>
)

const App = (
  <React.Fragment>
    <div>同級節點1</div>
    <div>同級節點2</div>
  </React.Fragment>
)

條件渲染

👇 Vue

<div v-if="show">條件渲染</div>
<div v-show="show">條件渲染</div>

👇 React

{
  show ? <div>條件渲染</div> : null
}

循環語句

👇 Vue

<ul>
  <li v-for="i in list" :key="i.id">{i.name}</li>
</ul>

👇 React

<ul>
  { list.map(i => <li key={i.id}>{i.name}</li>) }
</ul>

表單綁定

👇 Vue

<input v-model="value"/>

👇 React

<input value={value} onChange={onChange}/>

可以看出 ReactJSX語法 學習記憶成本更低一點 (當然Vue也不復雜),Vue 更語法糖一些

單向數據流與雙向綁定

Vue 中,我們使用 v-bindv-modal對數據進行綁定,無論是來自用戶操作導致的變更,還是在某個方法裏賦值都能夠直接更新數據,不需要手動進行 update 操作

this.data.msg = '直接修改數據後視圖更新'

React 中,數據流是單向的,即從父組件傳遞到子組件,而不允許子組件直接修改父組件的數據。需要調用set 方法更新,當 React 感應到 set 觸發時會再次調用 renderdom 進行刷新

msg = "Hello" // ❌ 錯誤寫法

setMsg('Hello'); // ✅ 來自hooks的set寫法 後面會介紹

🤔 Vue 本質上底層也是單向的數據流,只不過對使用者來說看起來是雙向的,如 v-model 本質也要 set

React Hooks

React HooksReact 16.8 版本中引入的特性,它可以讓我們在 函數組件 中使用狀態(state)和其他 React 特性

Hooks 本質是一些管理組件狀態和邏輯的 API ,它允許開發者在 函數式組件 中使用狀態、副作用和鉤子函數,可以更加方便地管理組件狀態、響應式地更新 DOM、使用上下文等

在沒有 Hooks 前, 函數組件 不能擁有狀態,只能做簡單功能的 UI(靜態元素展示),大家使用 類組件 來做狀態組件

因爲 函數組件 更加匹配 React 的設計理念 UI = f(data),也更有利於邏輯拆分與重用的組件表達形式,爲了能讓 函數組件 可以擁有自己的狀態,Hooks 應運而生

組件的邏輯複用

Hooks 出現之前,React 先後嘗試了 mixins混入HOC高階組件render-props等模式。但是都有各自的問題,比如 mixins 的數據來源不清晰,高階組件的嵌套問題等等

class 組件自身的問題

class 組件就像一個厚重的‘戰艦’ 一樣,大而全,提供了很多東西,有不可忽視的學習成本,比如各種生命週期,this 指向問題等等

useState

參數接受一個默認值,返回 [value, setValue] 的元組(就是約定好值的 JavaScript 數組),來讀取和修改數據

👇 不使用 Hooks 的靜態組件,當點擊修改數據,視圖不會重新渲染

function App() {
  let count = 1
  const add = () => count++ // 不會觸發重新渲染

  return <div onClick={add}>{count}</div>
}

👇 使用 useState

import { useState } from 'react'

function App() {
  let count = 1
  const [proxyCount, setProxyCount] = useState(count)
  const add = () => setProxyCount(proxyCount+1)

  return <div onClick={add}>{proxyCount}</div>
}

我們分析一下觸發數據修改的 函數組件行爲

組件會第二次渲染(useState 返回的數組第二項 setProxyCount() 被執行就會觸發重新渲染)

  1. 點擊按鈕,調用 setProxyCount(count + 1) 修改狀態,因爲狀態發生改變,所以,該組件會重新渲染

  2. 組件重新渲染時,會再次執行該組件中的代碼邏輯

  3. 再次調用 useState(1),此時 React 內部會拿到最新的狀態值而非初始值,比如,該案例中最新的狀態值爲 2

  4. 再次渲染組件,此時,獲取到的狀態 count 值爲 2

👆 也就是觸發重新渲染會讓 useState 也重新執行,但是 useState 的參數 (初始值) 只會在組件第一次渲染時生效

每次的渲染,useState 獲取到都是最新的狀態值,React 組件會記住每次最新的狀態值

useEffect

上面我們分析觸發組件重新渲染就可以發現,React 的函數組件沒有具體的生命週期鉤子

React 更希望我們把組件當作函數,而去關注函數的函數的副作用,而沒有實例化過程的鉤子

useEffect 就可以很好的幫助我們達到我們想要的效果:

  1. 處理組件第一次渲染時的回調,類似 Vue 中的 mounted
// 第二個參數傳一個空數組,表示沒有依賴,只會在第一次渲染時執行
useEffect(() ={
  alert('mounted');
}[])
  1. 通過依賴變更觸發的鉤子函數,只要有一項依賴發生變化就執行,類似 Vue 中的 watch
function Comp({ title }) {
  const [count, setCount] = useState(0);
  // 第二個參數指定一個數組,放入你想監聽的依賴:
  useEffect(() ={
    console.log('title or count has changed.')
  }[title, count])
}

原則上,函數中用到的所有依賴都應該放進數組裏

  1. 組件卸載時執行內部 return 的函數
import { useEffect } from "react"

const App = () ={

  useEffect(() ={
    const timerId = setInterval(() ={
      console.log('定時器在運行')
    }, 1000)

    return () ={ // 用來清理副作用的事情
      clearInterval(timerId)
    }
  }[])

  return <div>內部有定時器</div>
}

我們常見的副作用 1. 數據請求ajax發送 2. 手動修改dom 3. localstorage操作

自定義 Hooks

獲取滾動距離 y:

import { useState, useEffect } from "react"

export function useWindowScroll () {
  const [y, setY] = useState(0)

  useEffect(() ={
    const scrollHandler = () ={
      const h = document.documentElement.scrollTop
      setY(h)
    }
    window.addEventListener('scroll', scrollHandler)
    return () => window.removeEventListener('scroll', scrollHandler)
  })

  return [y]
}

使用:

const [y] = useWindowScroll()
return <div>{y}</div>

封裝的 Hooks 名稱也要用 use 開頭(這是一個約束)

狀態管理

React狀態管理 有很多,入門可以暫時不考慮

或者已有項目使用什麼再學習即可,和 Vuex 整體思路差不多

tic-tac-toe 井字棋遊戲

最後我們跟着 React 官方文檔實現一個井字棋遊戲來鞏固知識點

使用 Vite 創建項目

pnpm create vite react-tic-tac-toe --template react
cd react-tic-tac-toe
pnpm i
pnpm dev

👇 vite.config.js 非常簡潔

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [react()],
})

👇 修改入口文件 main.jsx

import React, { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import "./index.css";

import App from "./App";

const root = createRoot(document.getElementById("root"));
root.render(
  <StrictMode>
    <App />
  </StrictMode>
);

👇 util.js 計算當前棋局是否有獲勝

// 計算當前棋局是否有獲勝
export 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;
}

👇 Square.jsx 正方形按鈕組件

// 正方形按鈕組件
export default function Square({ value, onSquareClick }) {
  return (
    <button class onClick={onSquareClick}>
      {value}
    </button>
  );
}

👇 App.jsx

import { useState } from 'react';
import { calculateWinner } from './util.js'
import Square from './Square'

function Board({ xIsNext, squares, onPlay }) {
  function handleClick(i) {
    if (calculateWinner(squares) || squares[i]) {
      return;
    }
    const nextSquares = squares.slice();
    if (xIsNext) {
      nextSquares[i] = 'X';
    } else {
      nextSquares[i] = 'O';
    }
    // 執行父組件的落子事件
    onPlay(nextSquares);
  }

  const winner = calculateWinner(squares);
  let status;
  if (winner) {
    // 勝利提示
    status = '獲勝方是: ' + winner;
  } else {
    // 下一步提示
    status = 'Next player: ' + (xIsNext ? 'X' : 'O');
  }

  return (
    <>
      <div class>{status}</div>
      <div class>
        <Square value={squares[0]} onSquareClick={() => handleClick(0)} />
        <Square value={squares[1]} onSquareClick={() => handleClick(1)} />
        <Square value={squares[2]} onSquareClick={() => handleClick(2)} />
      </div>
      <div class>
        <Square value={squares[3]} onSquareClick={() => handleClick(3)} />
        <Square value={squares[4]} onSquareClick={() => handleClick(4)} />
        <Square value={squares[5]} onSquareClick={() => handleClick(5)} />
      </div>
      <div class>
        <Square value={squares[6]} onSquareClick={() => handleClick(6)} />
        <Square value={squares[7]} onSquareClick={() => handleClick(7)} />
        <Square value={squares[8]} onSquareClick={() => handleClick(8)} />
      </div>
    </>
  );
}

export default function Game() {
  const [history, setHistory] = useState([Array(9).fill(null)]);
  const [currentMove, setCurrentMove] = useState(0);
  const xIsNext = currentMove % 2 === 0;
  const currentSquares = history[currentMove];

  // 棋盤落子
  function handlePlay(nextSquares) {
    const nextHistory = [...history.slice(0, currentMove + 1), nextSquares];
    // 記錄落子歷史,用於恢復棋局
    setHistory(nextHistory);
    setCurrentMove(nextHistory.length - 1);
  }

  // 恢復棋局到第幾步
  function jumpTo(nextMove) {
    setCurrentMove(nextMove);
  }

  // 歷史落子列表按鈕展示,用於點擊恢復棋局
  const moves = history.map((squares, move) ={
    let description;
    if (move > 0) {
      description = 'Go to move #' + move;
    } else {
      description = 'Go to game start';
    }
    return (
      <li key={move}>
        <button onClick={() => jumpTo(move)}>{description}</button>
      </li>
    );
  });

  return (
    <div class>
      <div class>
        <Board xIsNext={xIsNext} squares={currentSquares} onPlay={handlePlay} />
      </div>
      <div class>
        <ol>{moves}</ol>
      </div>
    </div>
  );
}

深入學習任一前端框架都不容易,讓我們一起加油吧!

參考資料

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/tcY0H-rTe1b3Ar9qrEOlkQ