2023 Vue開發(fā)者的React入門
Vue 和 React 都是流行的 JavaScript 框架,它們?cè)诮M件化、數(shù)據(jù)綁定等方面有很多相似之處
本文默認(rèn)已有現(xiàn)代前端開發(fā)(Vue)背景,關(guān)于 組件化、前端路由、狀態(tài)管理 概念不會(huì)過多介紹
0基礎(chǔ)建議詳細(xì)閱讀 Thinking in React-官方文檔 了解 React 的設(shè)計(jì)哲學(xué)
- React 新文檔- https://react.dev
- React 中文文檔(翻譯中)- https://react.jscn.org
經(jīng)過本文的學(xué)習(xí)讓沒開發(fā)過 React 項(xiàng)目的 Vue 開發(fā)者可以上手開發(fā)現(xiàn)有的 React 項(xiàng)目,完成工作需求開發(fā)
React 新文檔
React 新文檔重新設(shè)計(jì)了導(dǎo)航結(jié)構(gòu),讓我們更加輕松地找到所需的文檔和示例代碼 不僅提供了基礎(chǔ)知識(shí)的介紹,還提供了更加詳細(xì)的原理介紹和最佳實(shí)踐,包括:React 組件的設(shè)計(jì)哲學(xué)、React Hooks的原理和用法等
并且提供了在線編輯和運(yùn)行的功能,方便開發(fā)者進(jìn)行測(cè)試和實(shí)驗(yàn)
?? 基于 函數(shù)組件
初學(xué)可以只學(xué) 函數(shù)組件,You Don't Need to Learn Class Components
?? interactive sandboxes 可交互沙箱,邊做邊學(xué)
Fork 可以單獨(dú)打開頁(yè)簽
JSX 與 SFC
- 在 Vue 中我們使用 單文件組件(SFC) 編寫組件模版 (雖然 Vue 也支持使用 JSX , 但是更鼓勵(lì)使用SFC)
- 在 React 中,JSX(JavaScript XML)是一種將HTML語(yǔ)法嵌入到 JavaScript 中的語(yǔ)法擴(kuò)展。它可以使得我們?cè)?nbsp;JavaScript 代碼中輕松地定義組件的結(jié)構(gòu)和樣式,從而提高代碼的可讀性和可維護(hù)性
雖然 React和 Vue 在組件定義方式上存在差異,但是它們的組件化思想是相似的
根節(jié)點(diǎn)
?? Vue
<template>
<div>同級(jí)節(jié)點(diǎn)1</div>
<div>同級(jí)節(jié)點(diǎn)2</div>
</template>
?? React
const App = (
<>
<div>同級(jí)節(jié)點(diǎn)1</div>
<div>同級(jí)節(jié)點(diǎn)2</div>
</>
)
const App = (
<React.Fragment>
<div>同級(jí)節(jié)點(diǎn)1</div>
<div>同級(jí)節(jié)點(diǎn)2</div>
</React.Fragment>
)
條件渲染
?? Vue
<div v-if="show">條件渲染</div>
<div v-show="show">條件渲染</div>
?? React
{
show ? <div>條件渲染</div> : null
}
循環(huán)語(yǔ)句
?? 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} notallow={onChange}/>
可以看出 React 的 JSX語(yǔ)法 學(xué)習(xí)記憶成本更低一點(diǎn)(當(dāng)然Vue也不復(fù)雜),Vue 更語(yǔ)法糖一些
單向數(shù)據(jù)流與雙向綁定
在 Vue 中,我們使用 v-bind、v-modal對(duì)數(shù)據(jù)進(jìn)行綁定,無論是來自用戶操作導(dǎo)致的變更,還是在某個(gè)方法里賦值都能夠直接更新數(shù)據(jù),不需要手動(dòng)進(jìn)行 update 操作
this.data.msg = '直接修改數(shù)據(jù)后視圖更新'
在 React 中,數(shù)據(jù)流是單向的,即從父組件傳遞到子組件,而不允許子組件直接修改父組件的數(shù)據(jù)。需要調(diào)用set 方法更新,當(dāng) React 感應(yīng)到 set 觸發(fā)時(shí)會(huì)再次調(diào)用 render 對(duì) dom 進(jìn)行刷新
msg = "Hello" // ? 錯(cuò)誤寫法
setMsg('Hello'); // ? 來自hooks的set寫法 后面會(huì)介紹
?? Vue 本質(zhì)上底層也是單向的數(shù)據(jù)流,只不過對(duì)使用者來說看起來是雙向的,如 v-model 本質(zhì)也要 set
React Hooks
React Hooks 是 React 16.8 版本中引入的特性,它可以讓我們?cè)?nbsp;函數(shù)組件 中使用狀態(tài)(state)和其他 React 特性
Hooks 本質(zhì)是一些管理組件狀態(tài)和邏輯的 API ,它允許開發(fā)者在 函數(shù)式組件 中使用狀態(tài)、副作用和鉤子函數(shù),可以更加方便地管理組件狀態(tài)、響應(yīng)式地更新DOM、使用上下文等
在沒有 Hooks 前, 函數(shù)組件 不能擁有狀態(tài),只能做簡(jiǎn)單功能的UI(靜態(tài)元素展示),大家使用 類組件 來做狀態(tài)組件
因?yàn)?nbsp;函數(shù)組件 更加匹配 React 的設(shè)計(jì)理念 UI = f(data),也更有利于邏輯拆分與重用的組件表達(dá)形式,為了能讓 函數(shù)組件 可以擁有自己的狀態(tài),Hooks 應(yīng)運(yùn)而生
組件的邏輯復(fù)用
在 Hooks 出現(xiàn)之前,React 先后嘗試了 mixins混入,HOC高階組件,render-props等模式。但是都有各自的問題,比如 mixins 的數(shù)據(jù)來源不清晰,高階組件的嵌套問題等等
class組件自身的問題
class組件就像一個(gè)厚重的‘戰(zhàn)艦’ 一樣,大而全,提供了很多東西,有不可忽視的學(xué)習(xí)成本,比如各種生命周期,this指向問題等等
useState
參數(shù)接受一個(gè)默認(rèn)值,返回 [value, setValue] 的元組(就是約定好值的 JavaScript 數(shù)組),來讀取和修改數(shù)據(jù)
?? 不使用 Hooks 的靜態(tài)組件,當(dāng)點(diǎn)擊修改數(shù)據(jù),視圖不會(huì)重新渲染
function App() {
let count = 1
const add = () => count++ // 不會(huì)觸發(fā)重新渲染
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>
}
我們分析一下觸發(fā)數(shù)據(jù)修改的 函數(shù)組件行為:
組件會(huì)第二次渲染(useState 返回的數(shù)組第二項(xiàng) setProxyCount() 被執(zhí)行就會(huì)觸發(fā)重新渲染)
- 點(diǎn)擊按鈕,調(diào)用 setProxyCount(count + 1) 修改狀態(tài),因?yàn)闋顟B(tài)發(fā)生改變,所以,該組件會(huì)重新渲染
- 組件重新渲染時(shí),會(huì)再次執(zhí)行該組件中的代碼邏輯
- 再次調(diào)用 useState(1),此時(shí) React 內(nèi)部會(huì)拿到最新的狀態(tài)值而非初始值,比如,該案例中最新的狀態(tài)值為 2
- 再次渲染組件,此時(shí),獲取到的狀態(tài) count 值為 2
?? 也就是觸發(fā)重新渲染會(huì)讓 useState 也重新執(zhí)行,但是 useState 的參數(shù)(初始值)只會(huì)在組件第一次渲染時(shí)生效
每次的渲染,useState 獲取到都是最新的狀態(tài)值,React 組件會(huì)記住每次最新的狀態(tài)值
useEffect
上面我們分析觸發(fā)組件重新渲染就可以發(fā)現(xiàn),React 的函數(shù)組件沒有具體的生命周期鉤子
React 更希望我們把組件當(dāng)作函數(shù),而去關(guān)注函數(shù)的函數(shù)的副作用,而沒有實(shí)例化過程的鉤子
useEffect 就可以很好的幫助我們達(dá)到我們想要的效果:
- 處理組件第一次渲染時(shí)的回調(diào),類似 Vue 中的 mounted
// 第二個(gè)參數(shù)傳一個(gè)空數(shù)組,表示沒有依賴,只會(huì)在第一次渲染時(shí)執(zhí)行
useEffect(() => {
alert('mounted');
}, [])
- 通過依賴變更觸發(fā)的鉤子函數(shù),只要有一項(xiàng)依賴發(fā)生變化就執(zhí)行,類似 Vue 中的 watch
function Comp({ title }) {
const [count, setCount] = useState(0);
// 第二個(gè)參數(shù)指定一個(gè)數(shù)組,放入你想監(jiān)聽的依賴:
useEffect(() => {
console.log('title or count has changed.')
}, [title, count])
}
原則上,函數(shù)中用到的所有依賴都應(yīng)該放進(jìn)數(shù)組里
- 組件卸載時(shí)執(zhí)行內(nèi)部 return 的函數(shù)
import { useEffect } from "react"
const App = () => {
useEffect(() => {
const timerId = setInterval(() => {
console.log('定時(shí)器在運(yùn)行')
}, 1000)
return () => { // 用來清理副作用的事情
clearInterval(timerId)
}
}, [])
return <div>內(nèi)部有定時(shí)器</div>
}
我們常見的副作用 1. 數(shù)據(jù)請(qǐng)求ajax發(fā)送 2. 手動(dòng)修改dom 3. localstorage操作
自定義 Hooks
獲取滾動(dòng)距離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 開頭(這是一個(gè)約束)
狀態(tài)管理
React 的 狀態(tài)管理 有很多,入門可以暫時(shí)不考慮
或者已有項(xiàng)目使用什么再學(xué)習(xí)即可,和 Vuex 整體思路差不多
tic-tac-toe 井字棋游戲
最后我們跟著 React 官方文檔實(shí)現(xiàn)一個(gè)井字棋游戲來鞏固知識(shí)點(diǎn)
使用 Vite 創(chuàng)建項(xiàng)目
pnpm create vite react-tic-tac-toe --template react
cd react-tic-tac-toe
pnpm i
pnpm dev
?? vite.config.js 非常簡(jiǎn)潔
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 計(jì)算當(dāng)前棋局是否有獲勝
// 計(jì)算當(dāng)前棋局是否有獲勝
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 className="square" 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';
}
// 執(zhí)行父組件的落子事件
onPlay(nextSquares);
}
const winner = calculateWinner(squares);
let status;
if (winner) {
// 勝利提示
status = '獲勝方是: ' + winner;
} else {
// 下一步提示
status = 'Next player: ' + (xIsNext ? 'X' : 'O');
}
return (
<>
<div className="status">{status}</div>
<div className="board-row">
<Square value={squares[0]} onSquareClick={() => handleClick(0)} />
<Square value={squares[1]} onSquareClick={() => handleClick(1)} />
<Square value={squares[2]} onSquareClick={() => handleClick(2)} />
</div>
<div className="board-row">
<Square value={squares[3]} onSquareClick={() => handleClick(3)} />
<Square value={squares[4]} onSquareClick={() => handleClick(4)} />
<Square value={squares[5]} onSquareClick={() => handleClick(5)} />
</div>
<div className="board-row">
<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];
// 記錄落子歷史,用于恢復(fù)棋局
setHistory(nextHistory);
setCurrentMove(nextHistory.length - 1);
}
// 恢復(fù)棋局到第幾步
function jumpTo(nextMove) {
setCurrentMove(nextMove);
}
// 歷史落子列表按鈕展示,用于點(diǎn)擊恢復(fù)棋局
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 className="game">
<div className="game-board">
<Board xIsNext={xIsNext} squares={currentSquares} onPlay={handlePlay} />
</div>
<div className="game-info">
<ol>{moves}</ol>
</div>
</div>
);
}
深入學(xué)習(xí)任一前端框架都不容易,讓我們一起加油吧!
參考資料
- React 新文檔- https://react.dev
- React 中文文檔(翻譯中)- https://react.jscn.org
- 給 Vue 開發(fā)的 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