2023 Vue 開發者的 React 入門
Vue
和 React
都是流行的 JavaScript
框架,它們在組件化、數據綁定等方面有很多相似之處
本文默認已有現代前端開發 (Vue
) 背景,關於 組件化、前端路由、狀態管理 概念不會過多介紹
0 基礎建議詳細閱讀 Thinking in React - 官方文檔 瞭解 React
的設計哲學
-
React 新文檔 - https://react.dev
-
React 中文文檔 (翻譯中)- https://react.jscn.org
經過本文的學習讓沒開發過 React
項目的 Vue
開發者可以上手開發現有的 React 項目,完成工作需求開發
React 新文檔
React
新文檔重新設計了導航結構,讓我們更加輕鬆地找到所需的文檔和示例代碼 不僅提供了基礎知識的介紹,還提供了更加詳細的原理介紹和最佳實踐,包括:React
組件的設計哲學、React Hooks
的原理和用法等
並且提供了在線編輯和運行的功能,方便開發者進行測試和實驗
👇 基於 函數組件
初學可以只學 函數組件,You Don't Need to Learn Class Components
👇 interactive sandboxes
可交互沙箱,邊做邊學
Fork
可以單獨打開頁籤
JSX 與 SFC
-
在
Vue
中我們使用單文件組件(SFC)
編寫組件模版 (雖然Vue
也支持使用JSX
, 但是更鼓勵使用SFC
) -
在
React
中,JSX(JavaScript XML)
是一種將 HTML 語法嵌入到JavaScript
中的語法擴展。它可以使得我們在JavaScript
代碼中輕鬆地定義組件的結構和樣式,從而提高代碼的可讀性和可維護性
雖然 React
和 Vue
在組件定義方式上存在差異,但是它們的組件化思想是相似的
根節點
👇 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}/>
可以看出 React
的 JSX語法
學習記憶成本更低一點 (當然Vue
也不復雜),Vue
更語法糖一些
單向數據流與雙向綁定
在 Vue
中,我們使用 v-bind
、v-modal
對數據進行綁定,無論是來自用戶操作導致的變更,還是在某個方法裏賦值都能夠直接更新數據,不需要手動進行 update
操作
this.data.msg = '直接修改數據後視圖更新'
在 React
中,數據流是單向的,即從父組件傳遞到子組件,而不允許子組件直接修改父組件的數據。需要調用set
方法更新,當 React
感應到 set
觸發時會再次調用 render
對 dom
進行刷新
msg = "Hello" // ❌ 錯誤寫法
setMsg('Hello'); // ✅ 來自hooks的set寫法 後面會介紹
🤔 Vue
本質上底層也是單向的數據流,只不過對使用者來說看起來是雙向的,如 v-model
本質也要 set
React Hooks
React Hooks
是 React 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()
被執行就會觸發重新渲染)
-
點擊按鈕,調用
setProxyCount(count + 1)
修改狀態,因爲狀態發生改變,所以,該組件會重新渲染 -
組件重新渲染時,會再次執行該組件中的代碼邏輯
-
再次調用
useState(1)
,此時React
內部會拿到最新的狀態值而非初始值,比如,該案例中最新的狀態值爲2
-
再次渲染組件,此時,獲取到的狀態
count
值爲2
👆 也就是觸發重新渲染會讓 useState
也重新執行,但是 useState
的參數 (初始值) 只會在組件第一次渲染時生效
每次的渲染,useState
獲取到都是最新的狀態值,React 組件會記住每次最新的狀態值
useEffect
上面我們分析觸發組件重新渲染就可以發現,React
的函數組件沒有具體的生命週期鉤子
React
更希望我們把組件當作函數,而去關注函數的函數的副作用,而沒有實例化過程的鉤子
useEffect
就可以很好的幫助我們達到我們想要的效果:
- 處理組件第一次渲染時的回調,類似
Vue
中的mounted
// 第二個參數傳一個空數組,表示沒有依賴,只會在第一次渲染時執行
useEffect(() => {
alert('mounted');
}, [])
- 通過依賴變更觸發的鉤子函數,只要有一項依賴發生變化就執行,類似
Vue
中的watch
function Comp({ title }) {
const [count, setCount] = useState(0);
// 第二個參數指定一個數組,放入你想監聽的依賴:
useEffect(() => {
console.log('title or count has changed.')
}, [title, count])
}
原則上,函數中用到的所有依賴都應該放進數組裏
- 組件卸載時執行內部
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>
);
}
深入學習任一前端框架都不容易,讓我們一起加油吧!
參考資料
-
React 新文檔 - https://react.dev
-
React 中文文檔 (翻譯中)- https://react.jscn.org
-
給 Vue 開發的 React 上手指南 - https://juejin.cn/post/6952545904087793678
-
無縫切換?從 Vue 到 React- https://zhuanlan.zhihu.com/p/609120596
-
How to Learn React in 2023- https://www.freecodecamp.org/news/how-to-learn-react-in-2023
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/tcY0H-rTe1b3Ar9qrEOlkQ