代碼下載
React 組件通訊原理
setState() 說明
setState() 是異步更新數(shù)據(jù)的,使用該語法時(shí),后面的 setState() 不要依賴于前面的 setState(),可以多次調(diào)用 setState() ,只會(huì)觸發(fā)一次重新渲染:
this.setState({ count: this.state.count + 1 })
console.log('count: ', this.state.count);
this.setState({ count: this.state.count + 1 })
console.log('count: ', this.state.count);
推薦使用 setState((state, props) => {}) 語法,參數(shù)state表示最新的state,參數(shù)props表示最新的props:
this.setState((state, props) => {
console.log('第一次 count: ', state.count);
return { count: state.count + 1 }
})
this.setState((state, props) => {
console.log('第二次 count: ', state.count);
return { count: state.count + 1 }
})
console.log('count: ', this.state.count);
第二個(gè)參數(shù)在狀態(tài)更新(頁面完成重新渲染)后立即執(zhí)行某個(gè)操作,語法 setState(updater[, callback]):
this.setState((state, props) => {
return { count: state.count + 1 }
}, () => {
console.log('count: ', this.state.count);
})
JSX 語法的轉(zhuǎn)化過程
JSX 僅僅是 createElement() 方法的語法糖(簡化語法),JSX 語法被 @babel/preset-react 插件編譯為 createElement() 方法。React 元素是一個(gè)對象,用來描述你希望在屏幕上看到的內(nèi)容:

組件更新機(jī)制
setState() 的兩個(gè)作用: 1. 修改 state 2. 更新組件(UI)
過程:父組件重新渲染時(shí),也會(huì)重新渲染子組件。但只會(huì)渲染當(dāng)前組件子樹(當(dāng)前組件及其所有子組件)

// 組件更新機(jī)制
class UpdateCom extends React.Component {
state = { color: 'skyblue'}
render() {
console.log('根組件 render');
return (
<div className='root' style={{ backgroundColor: this.state.color}}>
<p>根組件</p>
<button onClick={this.changeColor}>變色</button>
<div className='rootContainer'>
<LeftCom></LeftCom>
<RightCom></RightCom>
</div>
</div>
)
}
getColor = () => Math.floor(Math.random() * 256)
changeColor = () => {
this.setState({ color: `rgb(${this.getColor()}, ${this.getColor()}, ${this.getColor()})` })
}
}
class LeftCom extends React.Component {
state = { count: 0 }
render() {
console.log('左父組件 render');
return (
<div className='leftParent'>
<p>左父組件 count: {this.state.count}</p>
<button onClick={() => this.setState({count: this.state.count + 1})}>+1</button>
<div className='leftParentContainer'>
<LeftChildOne></LeftChildOne>
<LeftChildTwo></LeftChildTwo>
</div>
</div>
)
}
}
class RightCom extends React.Component {
state = { count: 0 }
render() {
console.log('右父組件 render');
return (
<div className='rightParent'>
<p>右父組件 count: {this.state.count}</p>
<button onClick={() => { this.setState({count: this.state.count + 1}) }}>+1</button>
<div className='rightParentContainer'>
<RightChildOne></RightChildOne>
<RightChildTwo></RightChildTwo>
</div>
</div>
)
}
}
const LeftChildOne = () => {
console.log('左子組件1 render');
return (<div className='leftChildOne'>左子組件1</div>)
}
const LeftChildTwo = () => {
console.log('左子組件2 render');
return (<div className='leftChildTwo'>左子組件2</div>)
}
const RightChildOne = () => {
console.log('右子組件1 render');
return (<div className='rightChildOne'>右子組件1</div>)
}
const RightChildTwo = () => {
console.log('右子組件2 render');
return (<div className='rightChildTwo'>右子組件2</div>)
}
ReactDOM.createRoot(document.getElementById('updateCom')).render(<UpdateCom></UpdateCom>)
css:
width: 800px;
}
.rootContainer, .leftParentContainer, .rightParentContainer {
display: flex;
}
.leftParent {
flex: 1;
background-color: green;
}
.leftParentContainer > div {
flex: 1;
background-color: blue;
margin: 10px;
}
.rightParent {
flex: 1;
background-color: cyan;
}
.rightParentContainer > div {
flex: 1;
background-color: pink;
margin: 10px;
}
組件性能優(yōu)化
1、 減輕 state,state 只存儲(chǔ)跟組件渲染相關(guān)的數(shù)據(jù)(比如:count / 列表數(shù)據(jù) / loading 等)
注意:不用做渲染的數(shù)據(jù)不要放在 state 中,比如定時(shí)器 id等,對于這種需要在多個(gè)方法中用到的數(shù)據(jù),應(yīng)該放在 this 中。
2、避免不必要的重新渲染,父組件更新會(huì)引起子組件也被更新,這種思路很清晰
問題:子組件沒有任何變化時(shí)也會(huì)重新渲染?如何避免不必要的重新渲染呢?
解決方式:使用鉤子函數(shù) shouldComponentUpdate(nextProps, nextState),通過返回值決定該組件是否重新渲染,返回 true 表示重新渲染,false 表示不重新渲染。更新階段的鉤子函數(shù),組件重新渲染前執(zhí)行 (shouldComponentUpdate -> render)。
class NumCom extends React.Component {
shouldComponentUpdate(nextProps, nextState) {
console.log('props: ', this.props, 'nextProps: ', nextProps);
// 根據(jù) props 數(shù)據(jù)是否變化,決定渲染
return this.props.num !== nextProps.num
}
render() {
console.log('NumCom render');
return (<><p>隨機(jī)數(shù):{this.props.num}</p></>)
}
}
class RefreshCom extends React.Component {
state = { num: 1 }
shouldComponentUpdate(nextProps, nextState) {
console.log('state: ', this.state, 'nextState: ', nextState);
// 根據(jù) state 數(shù)據(jù)是否變化,決定渲染
return this.state.num !== nextState.num
}
render() {
console.log('RefreshCom render');
return (
<>
<h4>避免不必要的重新渲染</h4>
<NumCom num={this.state.num}></NumCom>
<button onClick={() => this.setState({ num: Math.ceil(Math.random()*3)})}>生成隨機(jī)數(shù)</button>
</>
)
}
}
3、純組件,PureComponent 與 React.Component 功能相似,PureComponent 內(nèi)部自動(dòng)實(shí)現(xiàn)了 shouldComponentUpdate 鉤子,不需要手動(dòng)比較
原理:純組件內(nèi)部通過分別 對比 前后兩次 props 和 state 的值,來決定是否重新渲染組件。純組件內(nèi)部的對比是 shallow compare(淺層對比),對于值類型來說比較兩個(gè)值是否相同(直接賦值即可,沒有坑);對于引用類型來說只比較對象的引用(地址)是否相同。注意 state 或 props 中屬性值為引用類型時(shí),應(yīng)該創(chuàng)建新數(shù)據(jù),不要直接修改原數(shù)據(jù)!
class PureNum extends React.PureComponent {
render() {
console.log('PureNum render');
return (<><p>隨機(jī)數(shù):{this.props.num}</p></>)
}
}
class PureObjNum extends React.PureComponent {
render() {
console.log('PureObjNum render');
return (<><p>隨機(jī)數(shù):{this.props.numObj.num}</p></>)
}
}
class PureCom extends React.PureComponent {
state = {
num: 1,
numObj: { num: 1 }
}
render() {
console.log('PureCom render');
return (
<>
<h4>純組件</h4>
<PureNum num={this.state.num}></PureNum>
<button onClick={this.handle}>生成隨機(jī)數(shù)</button>
<PureObjNum numObj={this.state.numObj}></PureObjNum>
<button onClick={this.handleOne}>生成隨機(jī)數(shù)對象(修改原對象)</button> <br></br>
<button onClick={this.handleTwo}>生成隨機(jī)數(shù)對象(創(chuàng)建新對象)</button>
</>
)
}
handle = () => {
const number = Math.ceil(Math.random()*3)
console.log('num: ', this.state.num, ', new num: ', number);
this.setState({ num: number })
}
handleOne = () => {
// 修改原對象,錯(cuò)誤,因?yàn)椴粫?huì)刷新
const number = Math.ceil(Math.random()*3)
console.log('num: ', this.state.numObj.num, ', new num: ', number);
const obj = this.state.numObj
obj.num = number
this.setState({ numObj: obj})
}
handleTwo = () => {
// 創(chuàng)建新對象,正確,但是也得判斷,否則每次都會(huì)刷新
const number = Math.ceil(Math.random()*3)
console.log('num: ', this.state.numObj.num, ', new num: ', number);
if (number !== this.state.numObj.num) {
const obj = {...this.state.numObj, num: number}
this.setState({ numObj: obj})
}
}
}
虛擬DOM和Diff算法
React 更新視圖的思想是:只要 state 變化就重新渲染視圖,特點(diǎn)就是思路非常清晰。
問題:組件中只有一個(gè) DOM 元素需要更新時(shí),也得把整個(gè)組件的內(nèi)容重新渲染到頁面中?答案是否定的。理想狀態(tài)是部分更新,只更新變化的地方。那么 React 是如何做到部分更新的?虛擬 DOM 配合 Diff 算法。
虛擬 DOM 本質(zhì)上就是一個(gè) JS 對象,用來描述你希望在屏幕上看到的內(nèi)容(UI):

執(zhí)行過程
- 初次渲染時(shí),React 會(huì)根據(jù)初始state(Model),創(chuàng)建一個(gè)虛擬 DOM 對象(樹)。
- 根據(jù)虛擬 DOM 生成真正的 DOM,渲染到頁面中。
- 當(dāng)數(shù)據(jù)變化后(setState()),重新根據(jù)新的數(shù)據(jù),創(chuàng)建新的虛擬DOM對象(樹)。
- 與上一次得到的虛擬 DOM 對象,使用 Diff 算法 對比(找不同),得到需要更新的內(nèi)容。
- 最終,React 只將變化的內(nèi)容更新(patch)到 DOM 中,重新渲染到頁面。

虛擬DOM最大的特點(diǎn)是 脫離了瀏覽器的束縛,也就是意味著只要是能支持js的地方都可以用到react,所以為什么說react是可以進(jìn)行跨平臺(tái)的開發(fā)。
render 方法調(diào)用并不意味著瀏覽器中的重新渲染,render 方法調(diào)用僅僅說明創(chuàng)建了新的虛擬 DOM 對象(樹)要進(jìn)行diff 算法。
React 路由
現(xiàn)代的前端應(yīng)用大多都是 SPA(單頁應(yīng)用程序),也就是只有一個(gè) HTML 頁面的應(yīng)用程序。因?yàn)樗挠脩趔w
驗(yàn)更好、對服務(wù)器的壓力更小,所以更受歡迎。為了有效的使用單個(gè)頁面來管理原來多頁面的功能,前端路由
應(yīng)運(yùn)而生。
- 前端路由的功能是讓用戶從一個(gè)視圖(頁面)導(dǎo)航到另一個(gè)視圖(頁面)
- 前端路由是一套映射規(guī)則,在React中,是 URL路徑 與 組件 的對應(yīng)關(guān)系
- 使用React路由簡單來說,就是配置 路徑和組件(配對)
使用步驟
1、安裝,在項(xiàng)目根目錄執(zhí)行 npm i react-router-dom 或 yarn add react-router-dom
2、導(dǎo)入路由的5個(gè)核心組件:Router、Routes、Route、Link 或 NavLink
import { BrowserRouter as Router, Routes, Route, Link, NavLink } from 'react-router-dom'
3、使用 Router 組件包裹整個(gè)應(yīng)用(重要)
4、路由的本質(zhì)就是映射關(guān)系,用 Routes(就是router5里的Switch組件) 組件去盛放這層路由映射關(guān)系
5、使用 Link 或 NavLink 組件作為導(dǎo)航菜單(路由入口),這兩個(gè)組件主要包含兩個(gè)屬性:to屬性和replace屬性。
- to屬性: 用來設(shè)置跳轉(zhuǎn)到哪個(gè)路徑,相當(dāng)于是push操作;
- replace屬性:和to類似,也會(huì)跳轉(zhuǎn)到目標(biāo)路徑,但其執(zhí)行的是replace操作,設(shè)置了replace屬性會(huì)把路由棧里當(dāng)前路由替換掉
6、使用 Route 組件配置路由規(guī)則和要展示的組件(路由出口),注意 Route 組件必須包裹在 Routes 組件內(nèi)
- path屬性:用于設(shè)置匹配到的路徑
- element屬性:設(shè)置匹配到路徑后要渲染的組件;(在Router5里是使用component屬性)
// 使用步驟
const Home = () => (<p>首頁</p>)
const First = () => (<p>頁面一</p>)
const Second = () => (<p>頁面二</p>)
const NotFound = () => (<p>沒有發(fā)現(xiàn)該內(nèi)容</p>)
const RouterStep = () => {
return (
<>
<Router>
<p>Link</p>
<Link to='/home'>首頁</Link>
<Link to='/first'>頁面一</Link>
<Link to='/second' replace={true}>頁面二</Link>
<Link to='/xxx'>NotFound</Link>
<p>NavLink</p>
<NavLink to='/first' style={({isActive}) => { return isActive ? { color: 'blue' } : {} }}>頁面一</NavLink>
<NavLink to='/second' className={({isActive}) => { return isActive ? 'selected smoll' : ''}}>頁面二</NavLink>
<Routes>
<Route path='/home' element={<Home></Home>}></Route>
<Route path='/first' element={<First></First>}></Route>
<Route path='/second' element={<Second></Second>}></Route>
<Route path='*' element={<NotFound></NotFound>}></Route>
</Routes>
</Router>
</>
)
}
說明:
1、Router 組件包裹整個(gè)應(yīng)用,一個(gè) React 應(yīng)用只需要使用一次
2、兩種常用 Router:HashRouter 和 BrowserRouter
- HashRouter:使用 URL 的哈希值實(shí)現(xiàn)(localhost:3000/#/first)
- (推薦)BrowserRouter:使用 H5 的 history API 實(shí)現(xiàn)(localhost:3000/first)
3、Link 或 NavLink 組件:用于指定導(dǎo)航鏈接(a標(biāo)簽)。最終Link會(huì)編譯成a標(biāo)簽,而to屬性會(huì)被編譯成 a標(biāo)簽的href屬性。
4、Route 組件指定路由展示組件相關(guān)信息,Route組件寫在哪,渲染出來的組件就展示在哪
- path屬性:路由規(guī)則
- element屬性:展示的 React 元素
5、NavLink就是Link組件的樣式增強(qiáng)版,它與Link的用法基本相同,只不過就是多了幾個(gè)屬性進(jìn)行設(shè)置樣式,并且當(dāng)前選中的NavLink組件上會(huì)多出一個(gè)active類名
- style 屬性:接收一個(gè)函數(shù),函數(shù)接收一個(gè)對象,包含isActive屬性,表示當(dāng)前是否被選中;
- className 屬性:接收一個(gè)函數(shù),函數(shù)接收一個(gè)對象,包含isActive屬性,表示當(dāng)前是否被選中
6、通配符,在router6中,支持如下的幾種通配符:
- /xxx 確定的路徑名,如 : /home 表示home頁面組件能匹配上路徑只能是 /home ;
- /xxx/:xxx 動(dòng)態(tài)路徑名,:xxx會(huì)動(dòng)態(tài)變化的 。如:/home/:id 表示home頁面能匹配上 /home/11、/home/22、/home/abc、/home/xxx 等路徑;
- /xxx/:xxx/xxx動(dòng)態(tài)路徑名后跟確定路徑,如: /home/:id/abc 表示home頁面能匹配上 /home/11/abc、/home/22/abc 、/home/aa/abc 等路徑;
- /xxx/* 確定路徑名,后面可以跟多個(gè)子路徑,如:/home/* 表示home頁面能匹配上 /home、/home/12、/home/ab、/home/cd/123 等路徑;
- /xxx/:xxx/* 動(dòng)態(tài)路徑名后不限制子路徑,如:/home/:id/* 表示home頁面匹配 /home/11/abc/bcd、 /home/22/qwe 等路徑;
默認(rèn)路由
現(xiàn)在的路由都是點(diǎn)擊導(dǎo)航菜單后展示的,如何在進(jìn)入頁面的時(shí)候就展示呢?
默認(rèn)路由表示進(jìn)入頁面時(shí)就會(huì)匹配的路由,默認(rèn)路由path為 /:
{/* 默認(rèn)路由 */}
<Route path='/' element={<home></home>}></Route>
路由重定向
使用Navigate 組件來實(shí)現(xiàn)路由的重定向,只要這個(gè)組件出現(xiàn),就會(huì)執(zhí)行重定向跳轉(zhuǎn),并跳到其對應(yīng)的to路徑中,導(dǎo)入組件:
import { Navigate } from 'react-router-dom';
使用Navigate也很簡單,只要將想要被重定向的Route匹配關(guān)系中將element屬性里的原組件替換成Navigate組件即可;至于想要被重定向到哪個(gè)路徑,只需要將這個(gè)路徑寫入到Navigate組件中的to屬性即可:
<Route path='/' element={<Navigate to='/home' replace></Navigate>}></Route>
路由的執(zhí)行過程
- 點(diǎn)擊 Link 或 NavLink 組件(a標(biāo)簽),修改了瀏覽器地址欄中的 url 。
- React 路由監(jiān)聽到地址欄 url 的變化。
- React 路由內(nèi)部遍歷所有 Route 組件,使用路由規(guī)則( path )與 pathname 進(jìn)行匹配。
- 當(dāng)路由規(guī)則(path)能夠匹配地址欄中的 pathname 時(shí),就展示該 Route 組件的內(nèi)容。
匹配模式
- 精確匹配:只有當(dāng) path 和 pathname 完全匹配時(shí)才會(huì)展示該路由
- 模糊匹配:只要 pathname 以 path 開頭就會(huì)匹配成功
在router6中,不必再特別去設(shè)置exact屬性去進(jìn)行精確匹配組件了,因?yàn)閞outer6中已經(jīng)幫內(nèi)置進(jìn)去了。
如果想模糊匹配某一部分,在路徑后加 /*,增加一個(gè)匹配規(guī)則,將默認(rèn)路由設(shè)置為模糊匹配:
<Routes>
<Route path='/*' element={<Home></Home>}></Route>
</Routes>
路由懶加載
在路由中通常會(huì)定義很多不同的頁面。如果不應(yīng)用懶加載的話,很多頁面都會(huì)打包到同一個(gè)js文件中,文件將會(huì)異常的大。造成進(jìn)入首頁時(shí),需要加載的內(nèi)容過多,時(shí)間過長,在瀏覽器中可能會(huì)出現(xiàn)短暫的空白頁,從而降低用戶體驗(yàn),而運(yùn)用路由懶加載是將各個(gè)模塊分開打包,用戶查看的時(shí)候再加載對應(yīng)的模塊,減少加載用時(shí)。
懶加載就是延遲加載,也即在需要的時(shí)候才會(huì)進(jìn)行加載所需組件。
在react中借助React.lazy方法來進(jìn)行懶加載,用法如下 React.lazy(() => import("懶加載組件的路徑"))
1、定義一個(gè)單文件組件:
import React from "react";
export default class LazyOne extends React.Component {
render() {
return (
<>
<p>第一個(gè)懶加載組件</p>
</>
)
}
}
2、采用lazy懶加載的形式加載組件
const LazyOne = React.lazy(() => import('./LazyOne'))
3、增加路由匹配規(guī)則
<Route path='/lazyOne' element={<LazyOne></LazyOne>}></Route>
4、導(dǎo)航懶加載組件路由
<Link to='/lazyOne'>懶加載一</Link>
5、此時(shí)點(diǎn)擊路徑跳轉(zhuǎn),控制臺(tái)會(huì)報(bào)錯(cuò),這是因?yàn)楫?dāng)我們是用lazy去懶加載時(shí),應(yīng)該同時(shí)引入Suspense去包裹懶加載的組件。也就是說,lazy處理的懶加載組件一定要處于Suspense組件包裹下。
<React.Suspense>
<Router>
<Link to='/lazyOne'>懶加載一</Link>
<Routes>
<Route path='/lazyOne' element={<LazyOne></LazyOne>}></Route>
<Route path='*' element={<NotFound></NotFound>}></Route>
</Routes>
</Router>
</React.Suspense>
引入Suspense之后,再去點(diǎn)擊跳轉(zhuǎn)控制臺(tái)就不會(huì)再報(bào)錯(cuò)了。
6、當(dāng)網(wǎng)絡(luò)不好的時(shí)候進(jìn)行頁面跳轉(zhuǎn),會(huì)出現(xiàn)長時(shí)間的白屏現(xiàn)象??梢岳肧uspense組件的fallback屬性來進(jìn)行改善用戶體感,將fallback的值設(shè)置為antd的Spin組件:
<React.Suspense fallback={<Spin></Spin>}>
<Router>
<Link to='/lazyOne'>懶加載一</Link>
<Routes>
<Route path='/lazyOne' element={<LazyOne></LazyOne>}></Route>
<Route path='*' element={<NotFound></NotFound>}></Route>
</Routes>
</Router>
</React.Suspense>
fallback是Suspense組件的屬性,其作用是當(dāng)lazy處理的懶加載組件還沒有加載出來時(shí)要顯示的內(nèi)容。一般地都會(huì)往fallback屬性里傳入一個(gè)loading動(dòng)畫,用來緩解加載白屏的問題。
當(dāng)組件處于加載狀態(tài)時(shí),fallback組件是替換了整個(gè) Suspense 組件包裹的內(nèi)容,此時(shí)只顯示了Spin組件。但實(shí)際上在路由切換的時(shí)候是不應(yīng)該全部被 Suspense 的 fallback 替代的,因?yàn)槁酚汕袚Q時(shí)有的部分是不變的。造成此部分也被替換的原因是,Suspense 組件包裹的范圍太廣了。按照正常邏輯只要?jiǎng)討B(tài)加載的那部分被替換,其余不變的部分就仍然展示。對此需要縮小 Suspense 組件的包裹范圍,只讓它包裹住懶加載的組件即可。這時(shí)候可以使用 高階組件 或 render-props來配合處理懶加載組件:
// 高階組件
const withLoading = (WrapedComponent) => {
class LoadingCom extends React.Component {
render() {
return (<React.Suspense fallback={<Spin></Spin>}><WrapedComponent {...this.props}></WrapedComponent></React.Suspense>)
}
}
LoadingCom.displayName = `WithLoading${getDisplayName(WrapedComponent)}`
return LoadingCom
}
// render-props
function FuncLoading(props) {
return (
<React.Suspense fallback={<Spin></Spin>}>
{props.children}
</React.Suspense>
)
}
const RouteLazy = () => {
const LazyOne = React.lazy(() => import('./LazyOne'))
const LazyTwo = React.lazy(() => import('./LazyTwo'))
const LazyThree = React.lazy(() => import('./LazyThree'))
const LoadingOne = withLoading(LazyTwo)
return (
<>
<React.Suspense fallback={<Spin></Spin>}>
<Router>
<div>
<Link to='/lazyOne'>懶加載一</Link>
<Link to='/loadingOne'>懶加載二優(yōu)化</Link>
<Link to='/loadingTwo'>懶加載三優(yōu)化</Link>
</div>
<Routes>
<Route path='/lazyOne' element={<LazyOne></LazyOne>}></Route>
<Route path='/loadingOne' element={<LoadingOne></LoadingOne>}></Route>
<Route path='/loadingTwo' element={<FuncLoading><LazyThree></LazyThree></FuncLoading>}></Route>
<Route path='*' element={<NotFound></NotFound>}></Route>
</Routes>
</Router>
</React.Suspense>
</>
)
}
ReactDOM.createRoot(document.getElementById('routeLazy')).render(<RouteLazy></RouteLazy>)
其中 LazyTwo、LazyThree 組件與前面的 LazyOne 類似,略。
路由嵌套
在Router5中是將嵌套的子路由直接寫在父路由對應(yīng)的組件內(nèi)部的。這樣有個(gè)弊端就是Route太分散了,各個(gè)組件里都有Route。Router6.X對此做了一個(gè)調(diào)整,現(xiàn)在不用分散嵌套子路由,取而代之的是路由嵌套統(tǒng)一在一個(gè) Routes 組件下維護(hù)。
想要子路由生效,必須還得在其所屬的父路由里引入一個(gè)Outlet組件進(jìn)行路由占位。想要讓子路由在哪里展示,就把Outlet放到其內(nèi)部即可:
import { BrowserRouter as Router, Routes, Route, Link, NavLink, Outlet } from 'react-router-dom'
const ContentOne = () => { return (<p>內(nèi)容一</p>) }
const ContentTwo = () => { return (<p>內(nèi)容二</p>) }
const ContentThree = () => { return (<p>內(nèi)容三</p>) }
const NoContent = () => { return (<p>沒有內(nèi)容</p>) }
// 主體結(jié)構(gòu)組件
const MainBody = () => {
return (
<div className='mainBody'>
{/* 頭部區(qū)域 */}
<div className='top'>
路由嵌套
<Consumer>
{(data) => <button onClick={data.logout}>退出</button>}
</Consumer>
</div>
{/* 左邊欄區(qū)域 */}
<div className='leftAside'>
左邊欄
<ul>
<li><Link to='/'>內(nèi)容一</Link></li>
<li><Link to='/two'>內(nèi)容二</Link></li>
<li><Link to='/three'>內(nèi)容三</Link></li>
</ul>
</div>
{/* 內(nèi)容區(qū)域 */}
<div className='content'>
內(nèi)容
<Outlet></Outlet>
</div>
{/* 底部區(qū)域 */}
<div className='bottom'>底部區(qū)域</div>
</div>
)
}
// 登錄組件
const Login = () => {
return (
<Consumer>
{(data) => <button onClick={data.login}>登錄</button>}
</Consumer>
)
}
class RouteNest extends React.Component {
state = { isLogin: false }
render() {
const main = <Provider value={{ logout: () => this.setState({ isLogin: false }) }}><MainBody></MainBody></Provider>
const login = <Provider value={{ login: () => this.setState({ isLogin: true }) }}><Login></Login></Provider>
const first = (<>{this.state.isLogin ? main : login}</>)
return (
<Router>
<Routes>
{/* 父路由(默認(rèn)) */}
<Route path='/' element={first}>
{/* 子路由 */}
{/* 默認(rèn)子路由 */}
{/* <Route path='/' element={<ContentOne></ContentOne>}></Route> */}
<Route index element={<ContentOne></ContentOne>}></Route>
<Route path='/two' element={<ContentTwo></ContentTwo>}></Route>
<Route path='/three' element={<ContentThree></ContentThree>}></Route>
</Route>
<Route path='/login' element={<Login></Login>}></Route>
<Route path='*' element={<NotFound></NotFound>}></Route>
</Routes>
</Router>
)
}
}
ReactDOM.createRoot(document.getElementById('routeNest')).render(<RouteNest></RouteNest>)

如何設(shè)置默認(rèn)顯示的子路由?只需要在想要被設(shè)為默認(rèn)展示的子路由上寫一個(gè) index屬性即可(注意,被設(shè)置index的子路由不能再設(shè)置path屬性),那么當(dāng)訪問 '/' 就能做到默認(rèn)展示子路由了,跳轉(zhuǎn)路徑也的相應(yīng)調(diào)整為
<Link to='/'>內(nèi)容一</Link>。也可以直接將默認(rèn)子路由設(shè)置為<Route path='/' element={<ContentOne></ContentOne>}></Route>。
注意:路由嵌套時(shí),子路由的path路徑要以父路由的path名開頭,否則會(huì)報(bào)錯(cuò)。
編程式導(dǎo)航
如果希望通過JS代碼跳轉(zhuǎn),需要通過useNavigate獲取到navigate對象對象然后進(jìn)行后續(xù)操作。
import { useNavigate } from 'react-router-dom'
使用類組件開發(fā)則需要封裝高階函數(shù),也即手動(dòng)封裝withRouter:
function withRouter(WrapedComponent) {
function ComponentWithRouter(props) {
const navigate = useNavigate()
return <WrapedComponent {...props} router={navigate}></WrapedComponent>
}
ComponentWithRouter.displayName = `WithRouter${getDisplayName(WrapedComponent)}`
return ComponentWithRouter
}
可以直接調(diào)用 useNavigate 函數(shù)去返回一個(gè)navigate,然后調(diào)用navigate并將跳轉(zhuǎn)路徑作為參數(shù)傳進(jìn)去,例如:navigate('url路徑'),當(dāng)?shù)膎avigate傳入數(shù)字時(shí)(一般是-1),是跳到路由棧當(dāng)前路由前面幾個(gè)對應(yīng)的路由。 navigate里也可以傳第二個(gè)options參數(shù): {replace,state} 。replace參數(shù)代表替換當(dāng)前路由棧中的路由,state代表路由傳參:
function Red() { return (<div style={{ backgroundColor: 'red', width: '100px', height: '100px' }}></div>) }
function Green() { return (<div style={{ backgroundColor: 'green', width: '100px', height: '100px' }}></div>) }
// 類組件
class ClassComponent extends React.Component {
render() { return (<>
<p>類組件</p>
<button onClick={() => this.props.router('/red')}>紅</button>
<button onClick={() => this.props.router('/green', { replace: true, state: { data: '數(shù)據(jù)' }})}>綠</button>
<button onClick={() => this.props.router(-1)}>返回</button>
</>) }
}
// 函數(shù)組件
function FuncComponent() {
const navigate = useNavigate()
return (<>
<p>函數(shù)組件</p>
<button onClick={() => navigate('/red')}>紅</button>
<button onClick={() => navigate('/green', { replace: true, state: { data: '數(shù)據(jù)' }})}>綠</button>
<button onClick={() => navigate(-1)}>返回</button>
</>)
}
function FuncRoute(props) {
const WithRouterCom = withRouter(ClassComponent)
return (
<Router>
<WithRouterCom></WithRouterCom>
<FuncComponent></FuncComponent>
<Routes>
<Route path='/red' element={<Red></Red>}></Route>
<Route path='/green' element={<Green></Green>}></Route>
<Route path='*' element={<NotFound></NotFound>}></Route>
</Routes>
</Router>
)
}
ReactDOM.createRoot(document.getElementById('funcRoute')).render(<FuncRoute></FuncRoute>)
路徑如果以
/開頭代表 根路由+當(dāng)前路徑拼接成新路由; 不以/開頭表示在當(dāng)前路由后再跟上路徑拼接成新路由路徑。
navigate("/register") // 跳轉(zhuǎn)后路徑為 http://localhost:3000/register
navigate("register") // 跳轉(zhuǎn)后路徑為 http://localhost:3000/home/register
路由傳參
路由傳參一般用到三種方式:動(dòng)態(tài)路徑傳參、search參數(shù)傳遞、state傳遞參數(shù)。
動(dòng)態(tài)路徑傳參
如果將path在Route匹配時(shí)寫成 /detail/:id 那么 /detail/abc 、 /detail/123 都可以匹配到該Route對應(yīng)的組件并進(jìn)行顯示,針對動(dòng)態(tài)路徑傳參的這種方式,常用 useParams 來進(jìn)行獲取參數(shù)。
import { useParams } from 'react-router-dom'
function Detail() {
const params = useParams()
let value = ''
switch (params.id) {
case '1':
value = '一'
break;
case '2':
value = '二'
break
default:
break;
}
return (<p>詳情{value}</p>)
}
function ParamsCom() {
const navigate = useNavigate()
return (<>
<p>動(dòng)態(tài)路徑傳參</p>
<Link to='/detail/1'>詳情一</Link>
<button onClick={() => navigate('/detail/2')}>去詳情二</button>
</>)
}
function RouteParams() {
return (<Router>
<ParamsCom></ParamsCom>
<Routes>
<Route path='/detail/:id' element={<Detail></Detail>}></Route>
<Route path='*' element={<NotFound></NotFound>}></Route>
</Routes>
</Router>)
}
ReactDOM.createRoot(document.getElementById('routeParams')).render(<RouteParams></RouteParams>)
說明:動(dòng)態(tài)路徑可以配置多個(gè)參數(shù),如
<Route path='/detail/:id/:lastId' element={<Detail></Detail>}></Route>
search傳參
seatch 傳遞參數(shù)就是把我們要傳遞的參數(shù)拼接到url上進(jìn)行傳遞,具體方式是以 ? 開頭,鍵值對的形式傳參,每個(gè)參數(shù)之間用 & 連接,如 http://localhost:3000/list?page=1&size=10。
一般使用 useSearchParams 這個(gè)函數(shù)來獲取 search 參數(shù)(也可用useLocation,不過得額外再處理下search參數(shù)),注意 useSearchParams 返回的是個(gè)數(shù)組,且數(shù)組里是一個(gè) URLSearchParams 當(dāng)前值和 set 方法。并且取值時(shí)常借助 Object.fromEntries 這個(gè)方法:
import { useSearchParams } from 'react-router-dom'
function User() {
const [searchParams] = useSearchParams()
const params = Object.fromEntries(searchParams)
return (<p>id: {params.id}, name: {params.name}, age: {params.age}</p>)
}
function ParamsCom() {
const navigate = useNavigate()
return (<>
<p>search傳參</p>
<Link to='/user?id=1&name=zhangsan&age=18'>用戶一</Link>
<button onClick={() => navigate('/user?id=2&name=lisi&age=19')}>去用戶二</button>
</>)
}
function RouteParams() {
return (<Router>
<ParamsCom></ParamsCom>
<Routes>
<Route path='/user' element={<User></User>}></Route>
<Route path='*' element={<NotFound></NotFound>}></Route>
</Routes>
</Router>)
}
ReactDOM.createRoot(document.getElementById('routeParams')).render(<RouteParams></RouteParams>)
Object.entries()是將對象轉(zhuǎn)成一個(gè)自身可枚舉屬性的鍵值對數(shù)組,同樣也可以把鍵值對數(shù)組轉(zhuǎn)成了對象。Object.fromEntries是Object.entries反向,Object.fromEntries()方法把鍵值對列表轉(zhuǎn)換為一個(gè)對象。
state傳參
上面兩種方式的傳參都有一個(gè)缺點(diǎn)即傳遞的參數(shù)都在url路徑上體現(xiàn)了,并且涉及到復(fù)雜類型的參數(shù)傳遞就顯得很麻煩了。如果不想?yún)?shù)在url上面,且想傳些對象時(shí)就可以采取這種state傳參的方式。使用useLocation鉤子來獲取state參數(shù)。
import { useLocation } from 'react-router-dom'
function Content() {
const { state } = useLocation()
console.log('state: ', state);
return (<p>{state.content}</p>)
}
function ParamsCom() {
const navigate = useNavigate()
return (<>
<p>state傳參</p>
<Link to='/content' state={{content: '這是內(nèi)容一的內(nèi)容'}}>內(nèi)容一</Link>
<button onClick={() => navigate('/content', {state: { content: '這是內(nèi)容二的內(nèi)容' } })}>去內(nèi)容二</button>
</>)
}
function RouteParams() {
return (<Router>
<ParamsCom></ParamsCom>
<Routes>
<Route path='/content' element={<Content></Content>}></Route>
<Route path='*' element={<NotFound></NotFound>}></Route>
</Routes>
</Router>)
}
ReactDOM.createRoot(document.getElementById('routeParams')).render(<RouteParams></RouteParams>)
useRoutes 配置化路由
前面都是用Route組件的方式來實(shí)現(xiàn)路由配置的,但現(xiàn)在都提倡配置化,能不能把routes相關(guān)的配置都維護(hù)到一串?dāng)?shù)據(jù)里,不用以組件-屬性這種方式管理呢?
在 router5 中需引入的 react-router-config 這個(gè)三方庫來幫維護(hù)。但在 Router6 官方給提供了 useRoutes 這個(gè)函數(shù)。憑此可以將 routes 對象以參數(shù)形式傳入 useRoutes 中,此函數(shù)會(huì)根據(jù) routes 里的匹配關(guān)系對應(yīng)渲染成相關(guān)的路由組件。
routes其本身是一個(gè)數(shù)組,數(shù)組里維護(hù)各個(gè)匹配關(guān)系的對象。匹配關(guān)系這個(gè)對象里一般都有path、element、children、index屬性:
- path屬性就是組件對應(yīng)的路徑;
- element屬性就是要對應(yīng)的組件;
- index屬性就是默認(rèn)要展示的頁面組件;
- children屬性是路由嵌套時(shí)需要用也是一個(gè)數(shù)組,children數(shù)組里的屬性和外層一樣,子路由的配置就是在children屬性里維護(hù)的。
useRoutes的使用只要將路由配置對象 routes 傳到 useRoutes 函數(shù)即可:
// 軍事新聞組件
const Junshi = () => { return (<p>軍事新聞</p>) }
// 財(cái)經(jīng)新聞組件
const Caijin = () => { return (<p>財(cái)經(jīng)新聞</p>) }
// 體驗(yàn)新聞組件
const Tiyu = () => { return (<p>體育新聞</p>) }
// 布局組件
const Layout = () => {
return (
<div className='mainBody'>
{/* 頭部區(qū)域 */}
<div className='top'>useRoutes 配置化路由</div>
{/* 左邊欄區(qū)域 */}
<div className='leftAside'>
左邊欄
<ul>
<li><Link to='/'>軍事</Link></li>
<li><Link to='/caijin'>財(cái)經(jīng)</Link></li>
<li><Link to='/tiyu'>體育</Link></li>
</ul>
</div>
{/* 內(nèi)容區(qū)域 */}
<div className='content'>
內(nèi)容
<Outlet></Outlet>
</div>
{/* 底部區(qū)域 */}
<div className='bottom'>底部區(qū)域</div>
</div>
)
}
// 路由配置對象
const routes = [
{
path: '/',
element: <Layout></Layout>,
children: [
{
index: true,
element: <Junshi></Junshi>
},
{
path: '/caijin',
element: <Caijin></Caijin>
},
{
path: '/tiyu',
element: <Tiyu></Tiyu>
}
]
},
{
path: '*',
element: <NotFound></NotFound>
}
]
function RouteCom() {
console.log('RouteCom render');
// useRoutes 配置化路由
return (<>{useRoutes(routes)}</>)
}
function ConfigRoute() {
return (<Router><RouteCom></RouteCom></Router>)
}
ReactDOM.createRoot(document.getElementById('configRoute')).render(<ConfigRoute></ConfigRoute>)
useRoutes 的 rerender 問題
可以發(fā)現(xiàn) useRoutes 配置化路由只要切換路由 RouteCom 組件就會(huì)重新渲染,而換成如下方式則不會(huì)重新渲染:
function RouteCom() {
console.log('RouteCom render');
return <Routes>
<Route path='/' element={<Layout></Layout>}>
<Route index element={<Junshi></Junshi>}></Route>
<Route path='/caijin' element={<Caijin></Caijin>}></Route>
<Route path='/tiyu' element={<Tiyu></Tiyu>}></Route>
</Route>
<Route path='*' element={<NotFound></NotFound>}></Route>
</Routes>
}
由于 useRoutes 是通過 context 實(shí)現(xiàn)的,切換路由時(shí) context 共享出來的 value 值發(fā)生了變化,從而使得使用到這個(gè) context 的組件觸發(fā)了 rerender,這也就造成了使用 useRoutes 的 RouteCom 組件在切換路由時(shí)重新渲染了!