相關視頻:
黑馬程序員前端React18入門到實戰視頻教程,從react+hooks核心基礎到企業級項目開發實戰(B站評論、極客園項目等)及大廠面試全通關_嗶哩嗶哩_bilibili
一、React介紹
React由Meta公司開發,是一個用于 構建Web和原生交互界面的庫
React的優勢
相較于傳統基于DOM開發的優勢:
-
組件化的開發方式
-
不錯的性能
相較于其它前端框架的優勢:
-
豐富的生態
-
跨平臺支持
React的市場情況:全球最流行,大廠必備
開發環境創建
使用vite安裝:
npm create vite@latest my-app -- --template react
二、JSX基礎
什么是JSX
概念:JSX是JavaScript和XMl(HTML)的縮寫,表示在JS代碼中編寫HTML模版結構,它是React中構建UI的方式
const message = 'this is message'
?
function App(){return (<div><h1>this is title</h1>{message}</div>)
}
優勢:
-
HTML的聲明式模版寫法
-
JavaScript的可編程能力
JSX的本質
JSX并不是標準的JS語法,它是 JS的語法擴展,瀏覽器本身不能識別,需要通過解析工具做解析之后才能在瀏覽器中使用
JSX高頻場景-JS表達式
在JSX中可以通過
大括號語法{}
識別JavaScript中的表達式,比如常見的變量、函數調用、方法調用等等
-
使用引號傳遞字符串
-
使用JS變量
-
函數調用和方法調用
-
使用JavaScript對象
注意:if語句、switch語句、變量聲明不屬于表達式,不能出現在{}中
const message = 'this is message'
?
function getAge(){return 18
}
?
function App(){return (<div><h1>this is title</h1>{/* 字符串識別 */}{'this is str'}{/* 變量識別 */}{message}{/* 變量識別 */}{message}{/* 函數調用 渲染為函數的返回值 */}{getAge()}</div>)
}
JSX高頻場景-列表渲染
在JSX中可以使用原生js種的
map方法
實現列表渲染
const list = [{id:1001, name:'Vue'},{id:1002, name: 'React'},{id:1003, name: 'Angular'}
]
?
function App(){return (<ul>{list.map(item=><li key={item.id}>{item}</li>)}</ul>)
}
JSX高頻場景-條件渲染
在React中,可以通過邏輯與運算符&&、三元表達式(?:) 實現基礎的條件渲染
const flag = true
const loading = falsefunction App(){return (<>{flag && <span>this is span</span>}{loading ? <span>loading...</span>:<span>this is span</span>}</>)
}
JSX高頻場景-復雜條件渲染
需求:列表中需要根據文章的狀態適配 解決方案:自定義函數 + 判斷語句
const type = 1 ?// 0|1|3
?
function getArticleJSX(){if(type === 0){return <div>無圖模式模版</div>}else if(type === 1){return <div>單圖模式模版</div>}else(type === 3){return <div>三圖模式模版</div>}
}
?
function App(){return (<>{ getArticleJSX() }</>)
}
React的事件綁定
基礎實現
React中的事件綁定,通過語法
on + 事件名稱 = { 事件處理程序 }
,整體上遵循駝峰命名法
function App(){const clickHandler = ()=>{console.log('button按鈕點擊了')}return (<button onClick={clickHandler}>click me</button>)
}
使用事件參數
在事件回調函數中設置形參e即可
function App(){const clickHandler = (e)=>{console.log('button按鈕點擊了', e)}return (<button onClick={clickHandler}>click me</button>)
}
傳遞自定義參數
語法:事件綁定的位置改造成箭頭函數的寫法,在執行clickHandler實際處理業務函數的時候傳遞實參
function App(){const clickHandler = (name)=>{console.log('button按鈕點擊了', name)}return (<button onClick={()=>clickHandler('jack')}>click me</button>)
}
注意:不能直接寫函數調用,這里事件綁定需要一個函數引用
同時傳遞事件對象和自定義參數
語法:在事件綁定的位置傳遞事件實參e和自定義參數,clickHandler中聲明形參,注意順序對應
function App(){const clickHandler = (name,e)=>{console.log('button按鈕點擊了', name,e)}return (<button onClick={(e)=>clickHandler('jack',e)}>click me</button>)
}
React組件基礎使用
組件是什么
概念:一個組件就是一個用戶界面的一部分,它可以有自己的邏輯和外觀,組件之間可以互相嵌套,也可以服用多次
組件基礎使用
在React中,一個組件就是首字母大寫的函數,內部存放了組件的邏輯和視圖UI, 渲染組件只需要把組件當成標簽書寫即可
// 1. 定義組件
function Button(){return <button>click me</button>
}
?
// 2. 使用組件
function App(){return (<div>{/* 自閉和 */}<Button/>{/* 成對標簽 */}<Button></Button></div>)
}
組件狀態管理-useState
基礎使用
useState 是一個 React Hook(函數),它允許我們向組件添加一個
狀態變量
, 從而控制影響組件的渲染結果 和普通JS變量不同的是,狀態變量一旦發生變化組件的視圖UI也會跟著變化(數據驅動視圖)
function App(){const [ count, setCount ] = React.useState(0)return (<div><button onClick={()=>setCount(count+1)}>{ count }</button></div>)
}
狀態的修改規則
在React中狀態被認為是只讀的,我們應該始終
替換它而不是修改它
, 直接修改狀態不能引發視圖更新
修改對象狀態
對于對象類型的狀態變量,應該始終給set方法一個
全新的對象
來進行修改
組件的基礎樣式處理
React組件基礎的樣式控制有倆種方式,行內樣式和class類名控制
<div style={{ color:'red'}}>this is div</div>
.foo{color: red;
}
import './index.css'
?
function App(){return (<div><span className="foo">this is span</span></div>)
}
React表單控制
受控綁定
概念:使用React組件的狀態(useState)控制表單的狀態
function App(){const [value, setValue] = useState('')return (<input type="text" value={value} onChange={e => setValue(e.target.value)}/>)
}
非受控綁定
概念:通過獲取DOM的方式獲取表單的輸入數據
function App(){const inputRef = useRef(null)
?const onChange = ()=>{console.log(inputRef.current.value)}return (<input type="text" ref={inputRef}onChange={onChange}/>)
}
三、React組件通信
概念:組件通信就是
組件之間的數據傳遞
, 根據組件嵌套關系的不同,有不同的通信手段和方法
-
A-B 父子通信
-
B-C 兄弟通信
-
A-E 跨層通信
父子通信-父傳子
基礎實現
實現步驟
-
父組件傳遞數據 - 在子組件標簽上綁定屬性
-
子組件接收數據 - 子組件通過props參數接收數據
function Son(props){return <div>{ props.name }</div>
}
?
?
function App(){const name = 'this is app name'return (<div><Son name={name}/></div>)
}
props說明
props可以傳遞任意的合法數據,比如數字、字符串、布爾值、數組、對象、函數、JSX
props是只讀對象 子組件只能讀取props中的數據,不能直接進行修改, 父組件的數據只能由父組件修改
特殊的prop-chilren
場景:當我們把內容嵌套在組件的標簽內部時,組件會自動在名為children的prop屬性中接收該內容
父子通信-子傳父
核心思路:在子組件中調用父組件中的函數并傳遞參數
function Son({ onGetMsg }){const sonMsg = 'this is son msg'return (<div>{/* 在子組件中執行父組件傳遞過來的函數 */}<button onClick={()=>onGetMsg(sonMsg)}>send</button></div>)
}
?
?
function App(){const getMsg = (msg)=>console.log(msg)return (<div>{/* 傳遞父組件中的函數到子組件 */}<Son onGetMsg={ getMsg }/></div>)
}
兄弟組件通信
實現思路: 借助
狀態提升
機制,通過共同的父組件進行兄弟之間的數據傳遞
A組件先通過子傳父的方式把數據傳遞給父組件App
App拿到數據之后通過父傳子的方式再傳遞給B組件
// 1. 通過子傳父 A -> App
// 2. 通過父傳子 App -> B
?
import { useState } from "react"
?
function A ({ onGetAName }) {// Son組件中的數據const name = 'this is A name'return (<div>this is A compnent,<button onClick={() => onGetAName(name)}>send</button></div>)
}
?
function B ({ name }) {return (<div>this is B compnent,{name}</div>)
}
?
function App () {const [name, setName] = useState('')const getAName = (name) => {setName(name)}return (<div>this is App<A onGetAName={getAName} /><B name={name} /></div>)
}
?
export default App
跨層組件通信
實現步驟:
-
使用
createContext
方法創建一個上下文對象Ctx -
在頂層組件(App)中通過
Ctx.Provider
組件提供數據 -
在底層組件(B)中通過
useContext
鉤子函數獲取消費數據
// App -> A -> B
?
import { createContext, useContext } from "react"
?
// 1. createContext方法創建一個上下文對象
?
const MsgContext = createContext()
?
function A () {return (<div>this is A component<B /></div>)
}
?
function B () {// 3. 在底層組件 通過useContext鉤子函數使用數據const msg = useContext(MsgContext)return (<div>this is B compnent,{msg}</div>)
}
?
function App () {const msg = 'this is app msg'return (<div>{/* 2. 在頂層組件 通過Provider組件提供數據 */}<MsgContext.Provider value={msg}>this is App<A /></MsgContext.Provider></div>)
}
?
export default App
React副作用管理-useEffect
useEffect是一個React Hook函數,用于在React組件中創建不是由事件引起而是由渲染本身引起的操作(副作用), 比 如發送AJAX請求,更改DOM等等
說明:上面的組件中沒有發生任何的用戶事件,組件渲染完畢之后就需要和服務器要數據,整個過程屬于“只由渲染引起的操作”
基礎使用
需求:在組件渲染完畢之后,立刻從服務端獲取平道列表數據并顯示到頁面中
說明:
-
參數1是一個函數,可以把它叫做副作用函數,在函數內部可以放置要執行的操作
-
參數2是一個數組(可選參),在數組里放置依賴項,不同依賴項會影響第一個參數函數的執行,當是一個空數組的時候,副作用函數只會在組件渲染完畢之后執行一次
?warning:接口地址:http://geek.itheima.net/v1_0/channels
useEffect依賴說明
useEffect副作用函數的執行時機存在多種情況,根據傳入依賴項的不同,會有不同的執行表現
依賴項 | 副作用功函數的執行時機 |
---|---|
沒有依賴項 | 組件初始渲染 + 組件更新時執行 |
空數組依賴 | 只在初始渲染時執行一次 |
添加特定依賴項 | 組件初始渲染 + 依賴項變化時執行 |
清除副作用
概念:在useEffect中編寫的由渲染本身引起的對接組件外部的操作,社區也經常把它叫做副作用操作,比如在useEffect中開啟了一個定時器,我們想在組件卸載時把這個定時器再清理掉,這個過程就是清理副作用
說明:清除副作用的函數最常見的執行時機是在組件卸載時自動執行
import { useEffect, useState } from "react"function Son () {// 1. 渲染時開啟一個定時器useEffect(() => {const timer = setInterval(() => {console.log('定時器執行中...')}, 1000)return () => {// 清除副作用(組件卸載時)clearInterval(timer)}}, [])return <div>this is son</div>
}function App () {// 通過條件渲染模擬組件卸載const [show, setShow] = useState(true)return (<div>{show && <Son />}<button onClick={() => setShow(false)}>卸載Son組件</button></div>)
}export default App
自定義Hook實現
概念:自定義Hook是以
use打頭的函數
,通過自定義Hook函數可以用來實現邏輯的封裝和復用
// 封裝自定義Hook// 問題: 布爾切換的邏輯 當前組件耦合在一起的 不方便復用// 解決思路: 自定義hookimport { useState } from "react"function useToggle () {// 可復用的邏輯代碼const [value, setValue] = useState(true)const toggle = () => setValue(!value)// 哪些狀態和回調函數需要在其他組件中使用 returnreturn {value,toggle}
}// 封裝自定義hook通用思路// 1. 聲明一個以use打頭的函數
// 2. 在函數體內封裝可復用的邏輯(只要是可復用的邏輯)
// 3. 把組件中用到的狀態或者回調return出去(以對象或者數組)
// 4. 在哪個組件中要用到這個邏輯,就執行這個函數,解構出來狀態和回調進行使用function App () {const { value, toggle } = useToggle()return (<div>{value && <div>this is div</div>}<button onClick={toggle}>toggle</button></div>)
}export default App
React Hooks使用規則
-
只能在組件中或者其他自定義Hook函數中調用
-
只能在組件的頂層調用,不能嵌套在if、for、其它的函數中
四、Redux介紹
Redux 是React最常用的集中狀態管理工具,類似于Vue中的Pinia(Vuex),可以獨立于框架運行 作用:通過集中管理的方式管理應用的狀態
為什么要使用Redux?
-
獨立于組件,無視組件之間的層級關系,簡化通信問題
-
單項數據流清晰,易于定位bug
-
調試工具配套良好,方便調試
Redux快速體驗
1. 實現計數器
需求:不和任何框架綁定,不使用任何構建工具,使用純Redux實現計數器
使用步驟:
-
定義一個 reducer 函數 (根據當前想要做的修改返回一個新的狀態)
-
使用createStore方法傳入 reducer函數 生成一個store實例對象
-
使用store實例的 subscribe方法 訂閱數據的變化(數據一旦變化,可以得到通知)
-
使用store實例的 dispatch方法提交action對象 觸發數據變化(告訴reducer你想怎么改數據)
-
使用store實例的 getState方法 獲取最新的狀態數據更新到視圖中
代碼實現:
<button id="decrement">-</button>
<span id="count">0</span>
<button id="increment">+</button>
?
<script src="https://unpkg.com/redux@latest/dist/redux.min.js"></script>
?
<script>// 定義reducer函數 // 內部主要的工作是根據不同的action 返回不同的statefunction counterReducer (state = { count: 0 }, action) {switch (action.type) {case 'INCREMENT':return { count: state.count + 1 }case 'DECREMENT':return { count: state.count - 1 }default:return state}}// 使用reducer函數生成store實例const store = Redux.createStore(counterReducer)
?// 訂閱數據變化store.subscribe(() => {console.log(store.getState())document.getElementById('count').innerText = store.getState().count
?})// 增const inBtn = document.getElementById('increment')inBtn.addEventListener('click', () => {store.dispatch({type: 'INCREMENT'})})// 減const dBtn = document.getElementById('decrement')dBtn.addEventListener('click', () => {store.dispatch({type: 'DECREMENT'})})
</script>
2. Redux數據流架構
Redux的難點是理解它對于數據修改的規則, 下圖動態展示了在整個數據的修改中,數據的流向
為了職責清晰,Redux代碼被分為三個核心的概念,我們學redux,其實就是學這三個核心概念之間的配合,三個概念分別是:
-
state: 一個對象 存放著我們管理的數據
-
action: 一個對象 用來描述你想怎么改數據
-
reducer: 一個函數 根據action的描述更新state
Redux與React - 環境準備
Redux雖然是一個框架無關可以獨立運行的插件,但是社區通常還是把它與React綁定在一起使用,以一個計數器案例體驗一下Redux + React 的基礎使用
1. 配套工具
在React中使用redux,官方要求安裝倆個其他插件 - Redux Toolkit 和 react-redux
-
Redux Toolkit(RTK)- 官方推薦編寫Redux邏輯的方式,是一套工具的集合集,簡化書寫方式
-
react-redux - 用來 鏈接 Redux 和 React組件 的中間件
安裝配套工具:
npm i @reduxjs/toolkit react-redux
2. store目錄結構設計
-
通常集中狀態管理的部分都會單獨創建一個單獨的
store
目錄 -
應用通常會有很多個子store模塊,所以創建一個
modules
目錄,在內部編寫業務分類的子store -
store中的入口文件 index.js 的作用是組合modules中所有的子模塊,并導出store
Redux與React - 實現counter
1. 整體路徑熟悉
2. 使用React Toolkit 創建 counterStore
import { createSlice } from '@reduxjs/toolkit'
?
const counterStore = createSlice({// 模塊名稱獨一無二name: 'counter',// 初始數據initialState: {count: 1},// 修改數據的同步方法reducers: {increment (state) {state.count++},decrement(state){state.count--}}
})
// 結構出actionCreater
const { increment,decrement } = counter.actions
?
// 獲取reducer函數
const counterReducer = counterStore.reducer
?
// 導出
export { increment, decrement }
export default counterReducer
import { configureStore } from '@reduxjs/toolkit'
?
import counterReducer from './modules/counterStore'
?
export default configureStore({reducer: {// 注冊子模塊counter: counterReducer}
})
3. 為React注入store
react-redux負責把Redux和React 鏈接 起來,內置 Provider組件 通過 store 參數把創建好的store實例注入到應用中,鏈接正式建立
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
// 導入store
import store from './store'
// 導入store提供組件Provider
import { Provider } from 'react-redux'
?
ReactDOM.createRoot(document.getElementById('root')).render(// 提供store數據<Provider store={store}><App /></Provider>
)
4. React組件使用store中的數據
在React組件中使用store中的數據,需要用到一個鉤子函數 - useSelector,它的作用是把store中的數據映射到組件中,使用樣例如下:
5. React組件修改store中的數據
React組件中修改store中的數據需要借助另外一個hook函數 - useDispatch,它的作用是生成提交action對象的dispatch函數,使用樣例如下:
Redux與React - 提交action傳參
需求:組件中有倆個按鈕
add to 10
和add to 20
可以直接把count值修改到對應的數字,目標count值是在組件中傳遞過去的,需要在提交action的時候傳遞參數
實現方式:在reducers的同步修改方法中添加action對象參數,在調用actionCreater的時候傳遞參數,參數會被傳遞到action對象payload屬性上
Redux與React - 異步action處理
需求理解
實現步驟
-
創建store的寫法保持不變,配置好同步修改狀態的方法
-
單獨封裝一個函數,在函數內部return一個新函數,在新函數中 2.1 封裝異步請求獲取數據 2.2 調用同步actionCreater傳入異步數據生成一個action對象,并使用dispatch提交
-
組件中dispatch的寫法保持不變
代碼實現
測試接口地址: http://geek.itheima.net/v1_0/channels
import { createSlice } from '@reduxjs/toolkit'
import axios from 'axios'
?
const channelStore = createSlice({name: 'channel',initialState: {channelList: []},reducers: {setChannelList (state, action) {state.channelList = action.payload}}
})
?
?
// 創建異步
const { setChannelList } = channelStore.actions
const url = 'http://geek.itheima.net/v1_0/channels'
// 封裝一個函數 在函數中return一個新函數 在新函數中封裝異步
// 得到數據之后通過dispatch函數 觸發修改
const fetchChannelList = () => {return async (dispatch) => {const res = await axios.get(url)dispatch(setChannelList(res.data.data.channels))}
}
?
export { fetchChannelList }
?
const channelReducer = channelStore.reducer
export default channelReducer
import { useEffect } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { fetchChannelList } from './store/channelStore'
?
function App () {// 使用數據const { channelList } = useSelector(state => state.channel)useEffect(() => {dispatch(fetchChannelList())}, [dispatch])
?return (<div className="App"><ul>{channelList.map(task => <li key={task.id}>{task.name}</li>)}</ul></div>)
}
?
export default App
五、Zustand快速上手
store/index.js - 創建store
import { create } from 'zustand'
?
const useStore = create((set) => {return {count: 0,inc: () => {set(state => ({ count: state.count + 1 }))}}
})
?
export default useStore
app.js - 綁定組件
import useStore from './store/useCounterStore.js'
?
function App() {const { count, inc } = useStore()return <button onClick={inc}>{count}</button>
}
?
export default App
異步支持
對于異步操作的支持不需要特殊的操作,直接在函數中編寫異步邏輯,最后把接口的數據放到set函數中返回即可
store/index.js - 創建store
import { create } from 'zustand'
?
const URL = 'http://geek.itheima.net/v1_0/channels'
?
const useStore = create((set) => {return {count: 0,ins: () => {return set(state => ({ count: state.count + 1 }))},channelList: [],fetchChannelList: async () => {const res = await fetch(URL)const jsonData = await res.json()set({channelList: jsonData.data.channels})}}
})
?
export default useStore
app.js - 綁定組件
import { useEffect } from 'react'
import useChannelStore from './store/channelStore'
?
function App() {const { channelList, fetchChannelList } = useChannelStore()useEffect(() => {fetchChannelList()}, [fetchChannelList])
?return (<ul>{channelList.map((item) => (<li key={item.id}>{item.name}</li>))}</ul>)
}
?
export default App
切片模式
場景:當我們單個store比較大的時候,可以采用一種切片模式
進行模塊拆分再組合
拆分并組合切片
import { create } from 'zustand'
?
// 創建counter相關切片
const createCounterStore = (set) => {return {count: 0,setCount: () => {set(state => ({ count: state.count + 1 }))}}
}
?
// 創建channel相關切片
const createChannelStore = (set) => {return {channelList: [],fetchGetList: async () => {const res = await fetch(URL)const jsonData = await res.json()set({ channelList: jsonData.data.channels })}}
}
?
// 組合切片
const useStore = create((...a) => ({...createCounterStore(...a),...createChannelStore(...a)
}))
組件使用
function App() {const {count, inc, channelList, fetchChannelList } = useStore()return (<><button onClick={inc}>{count}</button><ul>{channelList.map((item) => (<li key={item.id}>{item.name}</li>))}</ul></>)
}
?
export default App
六、路由快速上手
1. 什么是前端路由
一個路徑 path 對應一個組件 component 當我們在瀏覽器中訪問一個 path 的時候,path 對應的組件會在頁面中進行渲染
安裝最新的ReactRouter包:
npm i react-router-dom
2. 快速開始
import React from 'react'
import ReactDOM from 'react-dom/client'const router = createBrowserRouter([{path:'/login',element: <div>登錄</div>},{path:'/article',element: <div>文章</div>}
])ReactDOM.createRoot(document.getElementById('root')).render(<RouterProvider router={router}/>
)
抽象路由模塊
路由導航
1. 什么是路由導航
路由系統中的多個路由之間需要進行路由跳轉,并且在跳轉的同時有可能需要傳遞參數進行通信
2. 聲明式導航
聲明式導航是指通過在模版中通過
<Link/>
組件描述出要跳轉到哪里去,比如后臺管理系統的左側菜單通常使用這種方式進行
語法說明:通過給組件的to屬性指定要跳轉到路由path,組件會被渲染為瀏覽器支持的a鏈接,如果需要傳參直接通過字符串拼接的方式拼接參數即可
3. 編程式導航
編程式導航是指通過 useNavigate
鉤子得到導航方法,然后通過調用方法以命令式的形式進行路由跳轉,比如想在登錄請求完畢之后跳轉就可以選擇這種方式,更加靈活
語法說明:通過調用navigate方法傳入地址path實現跳轉
導航傳參
嵌套路由配置
1. 什么是嵌套路由
在一級路由中又內嵌了其他路由,這種關系就叫做嵌套路由,嵌套至一級路由內的路由又稱作二級路由,例如:
2. 嵌套路由配置
實現步驟
使用
children
屬性配置路由嵌套關系使用
<Outlet/>
組件配置二級路由渲染位置
3. 默認二級路由
當訪問的是一級路由時,默認的二級路由組件可以得到渲染,只需要在二級路由的位置去掉path,設置index屬性為true
4. 404路由配置
場景:當瀏覽器輸入url的路徑在整個路由配置中都找不到對應的 path,為了用戶體驗,可以使用 404 兜底組件進行渲染
實現步驟:
-
準備一個NotFound組件
-
在路由表數組的末尾,以*號作為路由path配置路由
5. 倆種路由模式
各個主流框架的路由常用的路由模式有倆種,history模式和hash模式, ReactRouter分別由 createBrowerRouter 和 createHashRouter 函數負責創建
路由模式 | url表現 | 底層原理 | 是否需要后端支持 |
---|---|---|---|
history | url/login | history對象 + pushState事件 | 需要 |
hash | url/#/login | 監聽hashChange事件 | 不需要 |
七、React 內置 Hook
1. useReducer
基礎使用
作用: 讓 React 管理多個相對關聯的狀態數據
import { useReducer } from 'react'
?
// 1. 定義reducer函數,根據不同的action返回不同的新狀態
function reducer(state, action) {switch (action.type) {case 'INC':return state + 1case 'DEC':return state - 1default:return state}
}
?
function App() {// 2. 使用useReducer分派actionconst [state, dispatch] = useReducer(reducer, 0)return (<>{/* 3. 調用dispatch函數傳入action對象 觸發reducer函數,分派action操作,使用新狀態更新視圖 */}<button onClick={() => dispatch({ type: 'DEC' })}>-</button>{state}<button onClick={() => dispatch({ type: 'INC' })}>+</button></>)
}
?
export default App
更新流程
分派action傳參
做法:分派action時如果想要傳遞參數,需要在action對象中添加一個payload參數,放置狀態參數
// 定義reducer
?
import { useReducer } from 'react'
?
// 1. 根據不同的action返回不同的新狀態
function reducer(state, action) {console.log('reducer執行了')switch (action.type) {case 'INC':return state + 1case 'DEC':return state - 1case 'UPDATE':return state + action.payloaddefault:return state}
}
?
function App() {// 2. 使用useReducer分派actionconst [state, dispatch] = useReducer(reducer, 0)return (<>{/* 3. 調用dispatch函數傳入action對象 觸發reducer函數,分派action操作,使用新狀態更新視圖 */}<button onClick={() => dispatch({ type: 'DEC' })}>-</button>{state}<button onClick={() => dispatch({ type: 'INC' })}>+</button><button onClick={() => dispatch({ type: 'UPDATE', payload: 100 })}>update to 100</button></>)
}
?
export default App
2. useMemo
作用:它在每次重新渲染的時候能夠緩存計算的結果。
看個場景
下面我們的本來的用意是想基于count的變化計算斐波那契數列之和,但是當我們修改num狀態的時候,斐波那契求和函數也會被執行,顯然是一種浪費
// useMemo
// 作用:在組件渲染時緩存計算的結果
?
import { useState } from 'react'
?
function factorialOf(n) {console.log('斐波那契函數執行了')return n <= 0 ? 1 : n * factorialOf(n - 1)
}
?
function App() {const [count, setCount] = useState(0)// 計算斐波那契之和const sumByCount = factorialOf(count)
?const [num, setNum] = useState(0)
?return (<>{sum}<button onClick={() => setCount(count + 1)}>+count:{count}</button><button onClick={() => setNum(num + 1)}>+num:{num}</button></>)
}
?
export default App
useMemo緩存計算結果
思路: 只有count發生變化時才重新進行計算
import { useMemo, useState } from 'react'
?
function fib (n) {console.log('計算函數執行了')if (n < 3) return 1return fib(n - 2) + fib(n - 1)
}
?
function App() {const [count, setCount] = useState(0)// 計算斐波那契之和// const sum = fib(count)// 通過useMemo緩存計算結果,只有count發生變化時才重新計算const sum = useMemo(() => {return fib(count)}, [count])
?const [num, setNum] = useState(0)
?return (<>{sum}<button onClick={() => setCount(count + 1)}>+count:{count}</button><button onClick={() => setNum(num + 1)}>+num:{num}</button></>)
}
?
export default App
3. React.memo
作用:允許組件在props沒有改變的情況下跳過重新渲染
組件默認的渲染機制
默認機制:頂層組件發生重新渲染,這個組件樹的子級組件都會被重新渲染
// memo
// 作用:允許組件在props沒有改變的情況下跳過重新渲染
?
import { useState } from 'react'
?
function Son() {console.log('子組件被重新渲染了')return <div>this is son</div>
}
?
function App() {const [, forceUpdate] = useState()console.log('父組件重新渲染了')return (<><Son /><button onClick={() => forceUpdate(Math.random())}>update</button></>)
}
?
export default App
使用React.memo優化
機制:只有props發生變化時才重新渲染 下面的子組件通過 memo 進行包裹之后,返回一個新的組件MemoSon, 只有傳給MemoSon的props參數發生變化時才會重新渲染
import React, { useState } from 'react'
?
const MemoSon = React.memo(function Son() {console.log('子組件被重新渲染了')return <div>this is span</div>
})
?
function App() {const [, forceUpdate] = useState()console.log('父組件重新渲染了')return (<><MemoSon /><button onClick={() => forceUpdate(Math.random())}>update</button></>)
}
?
export default App
props變化重新渲染
import React, { useState } from 'react'
?
const MemoSon = React.memo(function Son() {console.log('子組件被重新渲染了')return <div>this is span</div>
})
?
function App() {console.log('父組件重新渲染了')
?const [count, setCount] = useState(0)return (<><MemoSon count={count} /><button onClick={() => setCount(count + 1)}>+{count}</button></>)
}
?
export default App
props的比較機制
對于props的比較,進行的是‘淺比較’,底層使用
Object.is
進行比較,針對于對象數據類型,只會對比倆次的引用是否相等,如果不相等就會重新渲染,React并不關心對象中的具體屬性
import React, { useState } from 'react'
?
const MemoSon = React.memo(function Son() {console.log('子組件被重新渲染了')return <div>this is span</div>
})
?
function App() {// const [count, setCount] = useState(0)const [list, setList] = useState([1, 2, 3])return (<><MemoSon list={list} /><button onClick={() => setList([1, 2, 3])}>{JSON.stringify(list)}</button></>)
}
?
export default App
說明:雖然倆次的list狀態都是 [1,2,3]
, 但是因為組件App倆次渲染生成了不同的對象引用list,所以傳給MemoSon組件的props視為不同,子組件就會發生重新渲染
自定義比較函數
如果上一小節的例子,我們不想通過引用來比較,而是完全比較數組的成員是否完全一致,則可以通過自定義比較函數來實現
import React, { useState } from 'react'
?
// 自定義比較函數
function arePropsEqual(oldProps, newProps) {console.log(oldProps, newProps)return (oldProps.list.length === newProps.list.length &&oldProps.list.every((oldItem, index) => {const newItem = newProps.list[index]console.log(newItem, oldItem)return oldItem === newItem}))
}
?
const MemoSon = React.memo(function Son() {console.log('子組件被重新渲染了')return <div>this is span</div>
}, arePropsEqual)
?
function App() {console.log('父組件重新渲染了')const [list, setList] = useState([1, 2, 3])return (<><MemoSon list={list} /><button onClick={() => setList([1, 2, 3])}>內容一樣{JSON.stringify(list)}</button><button onClick={() => setList([4, 5, 6])}>內容不一樣{JSON.stringify(list)}</button></>)
}
?
export default App
4. useCallback
看個場景
上一小節我們說到,當給子組件傳遞一個引用類型
prop的時候,即使我們使用了memo
函數依舊無法阻止子組件的渲染,其實傳遞prop的時候,往往傳遞一個回調函數更為常見,比如實現子傳父,此時如果想要避免子組件渲染,可以使用 useCallback
緩存回調函數
// useCallBack
?
import { memo, useState } from 'react'
?
const MemoSon = memo(function Son() {console.log('Son組件渲染了')return <div>this is son</div>
})
?
function App() {const [, forceUpate] = useState()console.log('父組件重新渲染了')const onGetSonMessage = (message) => {console.log(message)}
?return (<div><MemoSon onGetSonMessage={onGetSonMessage} /><button onClick={() => forceUpate(Math.random())}>update</button></div>)
}
?
export default App
useCallback緩存函數
useCallback緩存之后的函數可以在組件渲染時保持引用穩定,也就是返回同一個引用
// useCallBack
?
import { memo, useCallback, useState } from 'react'
?
const MemoSon = memo(function Son() {console.log('Son組件渲染了')return <div>this is son</div>
})
?
function App() {const [, forceUpate] = useState()console.log('父組件重新渲染了')const onGetSonMessage = useCallback((message) => {console.log(message)}, [])
?return (<div><MemoSon onGetSonMessage={onGetSonMessage} /><button onClick={() => forceUpate(Math.random())}>update</button></div>)
}
?
export default App
5. forwardRef(已廢棄)
作用:允許組件使用ref將一個DOM節點暴露給父組件
import { forwardRef, useRef } from 'react'
?
const MyInput = forwardRef(function Input(props, ref) {return <input {...props} type="text" ref={ref} />
}, [])
?
function App() {const ref = useRef(null)
?const focusHandle = () => {console.log(ref.current.focus())}
?return (<div><MyInput ref={ref} /><button onClick={focusHandle}>focus</button></div>)
}
?
export default App
從 React 19 開始, ref 可作為 prop 使用 。在 React 18 及更早版本中,需要通過 forwardRef 來獲取 ref 。
6. useImperativeHandle
作用:如果我們并不想暴露子組件中的DOM而是想暴露子組件內部的方法
import { forwardRef, useImperativeHandle, useRef } from 'react'
?
const MyInput = forwardRef(function Input(props, ref) {// 實現內部的聚焦邏輯const inputRef = useRef(null)const focus = () => inputRef.current.focus()
?// 暴露子組件內部的聚焦方法useImperativeHandle(ref, () => {return {focus,}})
?return <input {...props} ref={inputRef} type="text" />
})
?
function App() {const ref = useRef(null)
?const focusHandle = () => ref.current.focus()
?return (<div><MyInput ref={ref} /><button onClick={focusHandle}>focus</button></div>)
}
?
export default App
八、Hooks 與 TypeScript
useState
簡單場景
簡單場景下,可以使用TS的自動推斷機制,不用特殊編寫類型注解,運行良好
const [val, toggle] = React.useState(false)
?
// `val` 會被自動推斷為布爾類型
// `toggle` 方法調用時只能傳入布爾類型
復雜場景
復雜數據類型,useState支持通過泛型參數
指定初始參數類型以及setter函數的入參類型
type User = {name: stringage: number
}
const [user, setUser] = React.useState<User>({name: 'jack',age: 18
})
// 執行setUser
setUser(newUser)
// 這里newUser對象只能是User類型
沒有具體默認值
實際開發時,有些時候useState的初始值可能為null或者undefined,按照泛型的寫法是不能通過類型校驗的,此時可以通過完整的類型聯合null或者undefined類型即可
type User = {name: Stringage: Number
}
const [user, setUser] = React.useState<User>(null)
// 上面會類型錯誤,因為null并不能分配給User類型
?
const [user, setUser] = React.useState<User | null>(null)
// 上面既可以在初始值設置為null,同時滿足setter函數setUser的參數可以是具體的User類型
useRef
在TypeScript的環境下,
useRef
函數返回一個只讀
或者可變
的引用,只讀的場景常見于獲取真實dom,可變的場景,常見于緩存一些數據,不跟隨組件渲染,下面分倆種情況說明
獲取dom
獲取DOM時,通過泛型參數指定具體的DOM元素類型即可
function Foo() {// 盡可能提供一個具體的dom type, 可以幫助我們在用dom屬性時有更明確的提示// divRef的類型為 RefObject<HTMLDivElement>const inputRef = useRef<HTMLDivElement>(null)
?useEffect(() => {inputRef.current.focus()})
?return <div ref={inputRef}>etc</div>
}
// 如果你可以確保divRef.current 不是null,也可以在傳入初始值的位置// 添加非null標記
const divRef = useRef<HTMLDivElement>(null!)
// 不再需要檢查`divRef.current` 是否為null
doSomethingWith(divRef.current)
穩定引用存儲器
當做為可變存儲容器使用的時候,可以通過泛型參數
指定容器存入的數據類型, 在還為存入實際內容時通常把null作為初始值,所以依舊可以通過聯合類型做指定
interface User {age: number
}
?
function App(){const timerRef = useRef<number | undefined>(undefined)const userRes = useRef<User | null> (null)useEffect(()=>{timerRef.current = window.setInterval(()=>{console.log('測試')},1000)return ()=>clearInterval(timerRef.current)})return <div> this is app</div>
}
九、Component 與 TypeScript
為Props添加類型
props作為React組件的參數入口,添加了類型之后可以限制參數輸入以及在使用props有良好的類型提示
使用interface接口
interface Props {className: string
}
?
export const Button = (props:Props)=>{const { className } = propsreturn <button className={ className }>Test</button>
}
使用自定義類型Type
type Props = {className: string
}
?
export const Button = (props:Props)=>{const { className } = propsreturn <button className={ className }>Test</button>
}
為Props的chidren屬性添加類型
children屬性和props中其他的屬性不同,它是React系統中內置的,其它屬性我們可以自由控制其類型,children屬性的類型最好由React內置的類型提供,兼容多種類型
type Props = {children: React.ReactNode
}
?
export const Button = (props: Props)=>{const { children } = propsreturn <button>{ children }</button>
}
說明:React.ReactNode是一個React內置的聯合類型,包括 React.ReactElement
、string
、number
React.ReactFragment
、React.ReactPortal
、boolean
、 null
、undefined
為事件prop添加類型
// props + ts
type Props = {onGetMsg?: (msg: string) => void
}
?
function Son(props: Props) {const { onGetMsg } = propsconst clickHandler = () => {onGetMsg?.('this is msg')}return <button onClick={clickHandler}>sendMsg</button>
}
?
function App() {const getMsgHandler = (msg: string) => {console.log(msg)}return (<><Son onGetMsg={(msg) => console.log(msg)} /><Son onGetMsg={getMsgHandler} /></>)
}
?
export default App
為事件handle添加類型
為事件回調添加類型約束需要使用React內置的泛型函數來做,比如最常見的鼠標點擊事件和表單輸入事件:
function App(){const changeHandler: React.ChangeEventHandler<HTMLInputElement> = (e)=>{console.log(e.target.value)}const clickHandler: React.MouseEventHandler<HTMLButtonElement> = (e)=>{console.log(e.target)}
?return (<><input type="text" onChange={ changeHandler }/><button onClick={ clickHandler }> click me!</button></>)
}