前言
花了一點時間把react-router系統的整理了一下,包括常用元件的功能原理以及基本實現方式, 文中所貼出來的程式碼都是每個元件的核心原理的實現,與原始碼會有略有不同,敬請注意,原始碼地址均已提供詳細的連線,點選即可跳轉。放心食用。
渲染方式
- children
- component
- render
優先順序:
這三種渲染方式是互斥的,同時存在的情況下: children
> component
> render
;
這是原始碼中關於優先順序部分的程式碼;
注意事項
children
和render
是隻能以匿名函式
的形式傳入要展示的節點,component
則不需要。component
和render
需要path
匹配上以後才能展示,children
則不論是否匹配都會展示。component
不建議以匿名函式
的形式傳入要展示的節點,因為渲染的時候會呼叫React.createElement
,如果使用匿名函式
的形式,每次都會生成新的type,導致子元件出現頻繁掛載和解除安裝的問題,children
和render
則不會;
有興趣的可以嘗試執行一下程式碼;
'use strict';
import React, { useState, useEffect } from 'react';
import { Router, Route } from 'react-router';
const Child = (props) => {
useEffect(() => {
console.log("掛載");
return () => console.log("解除安裝");
}, []);
return <div>Child - {props.count}</div>
}
class ChildFunc extends React.Component {
componentDidMount() {
console.log("componentDidMount");
}
componentWillUnmount() {
console.log("componentWillUnmount");
}
render() {
return <div>
ChildFunc - {this.props.count}
</div>
}
}
const Index = (props) => {
const [count, setCount] = useState(0);
return <div>
<button onClick={() => setCount((state) => state + 1)}>add</button>
<p>chick change count{count}</p>
<Router >
{/* bad 觀察一下掛載和解除安裝函式的log*/}
<Route component={() => <Child count={count} />} />
<Route component={() => <ChildFunc count={count} />} />
{/* good 這才是正確的開啟方式 觀察一下掛載和解除安裝函式的log*/}
<Route render={() => <Child count={count} />} />
<Route render={() => <ChildFunc count={count} />} />
{/* 觀察一下掛載和解除安裝函式的log 這種也是可以的但是children不需要匹配path,慎用*/}
<Route children={() => <ChildFunc count={count} />} />
<Route children={() => <Child count={count} />} />
</Router>
</div>
};
export default Index;
Link元件
link 本質上就是一個a標籤,但是直接使用href屬性點選的時候會有抖動需要使用命令的方式跳轉,原始碼中對其追加了部分屬性和功能,並且對引數to
和click
事件進行了處理。
原始碼請移步
'use strict';
import React, { useContext } from 'react'
import RouterContext from './RouterContext'
export default function Link({ to, children }) {
const { history } = useContext(RouterContext)
const handle = e => {
// 防止抖動所以禁掉預設行為命令形式跳轉
e.preventDefault();
history.push(to)
};
return <a href={to} onClick={handle}>{children}</a>
};
BrowserRouter元件
這個元件是react-router
的最上層元件,主要作用決定路由系統使用何種路由。
檢視原始碼請移步
'use strict'
import React, { PureComponent } from 'react';
import { createBrowserHistory } from 'history'
import Router from "./Router"
export default class BrowserRouter extends PureComponent {
constructor(props) {
super(props);
this.history = createBrowserHistory();
}
render() {
return <Router history={this.history}>{this.props.children}</Router>
}
};
RouterContext.js 檔案
因為路由元件可以和普通元素節點進行巢狀,並不能很好的確定具體的層級關係,所以我們依舊選擇跨層級資料殘敵的方式來實現。宣告並匯出RouterContext拆分成獨立檔案會使邏輯更加清晰。
原始碼並沒有直接使用createContext
而是又包了一層createNamedContext
為生成的context新增了一個displayName.
'use strict';
import React from 'react'
const RouterContext = React.createContext();
export default RouterContext;
Router.js 檔案
Router檔案主要作用:
- 通過RouterContext向下傳遞
history
、location
、match
等屬性; - 通過
history.listen
監聽頁面的location
的變化,並向下傳遞location
方便Route
元件以及Switch
元件進行匹配;
原始碼
'use strict'
import React, { PureComponent } from 'react';
import RouterContext from 'RouterContext'
export default class Router extends PureComponent {
static computeRootMatch(pathname) {
return { path: "/", url: "/", params: {}, isExact: pathname === "/" };
}
constructor() {
super(props)
this.state = {
location: props.history.location
}
this.unlinsten = props.history.listen((location) => {
this.setState({ location })
})
}
componentWillUnmount() {
this.unlinsten();
}
render() {
return (
<RouterContext.Provider value={{
history: this.props.history,
location: this.state.location,
match: Router.computeRouteMatch(this.state.location.pathname)
}} >
{this.props.children}
</RouterContext.Provider>
)
}
}
Route 元件
route
元件主要是負責match
的處理並返回需要渲染的component
元件,這個match
可以是上層Switch
元件傳下來的computedMatch
, 如果上層沒有使用Switch
元件,則判定Route
元件接收到的path
屬性是否存在 這在則與location.pathname
進行比對如果匹配上就展示展示不上就不不展示,path
也可以為空,如果為空就直接使用context.match
;
'use strict'
import React, { PureComponent } from 'react';
import matchPath from './matchPath';
import RouterContext from './RouterContext';
export default class Route extends PureComponent {
render() {
return <RouterContext.Consumer >
{(context) => {
const { path, children, component, render, computedMatch } = this.props;
const { location } = context;
// 當match時,說明當前匹配成功
const match = computedMatch
? computedMatch
: path
? matchPath(location.pathname, this.props)
: context.match;
const props = { ...context, match }
// 匹配成功以後要根據children > component > render的優先順序來渲染
return <RouterContext.Provider value={props}>
{
match
? children
? typeof children === "function" ? children(props) : children
: component ? React.createElement(component, props)
: render ? render(props) : null
: typeof children === "function" ? children(props) : null
}
</RouterContext.Provider>
}}
</RouterContext.Consumer>
}
}
注意:
- 上述程式碼中反覆提到的
match
就是我們路由掛載引數的那個match
;- 我們在人
return
的component
的地方給返回值有包裹了一層RouterContext.Provider
,原因是我們在外部使用useRouteMatch
和useParams
獲取match
的時候,context
獲取到的match
其實是Router.js
檔案傳遞下來的初始值,但是我們這裡需要獲取Route
元件裡面的match
值,所以要在包一層,這裡是利用了context
的就近取值
的特性;
switch元件
Switch寓意為獨佔路由,作用:匹配路由並且只渲染匹配到的第一個route
或者redirect
;
因為以上原因,例如404這樣不寫path屬性的元件一定要放在最後,不然404元件一旦被匹配,那之後的子元件都不會再匹配了;
和Route元件的區別在於,Switch是控制顯示哪一個Route 元件,而Route 元件空的是當前這個Route元件下的component是否展示
'use strict'
import React, { PureComponent } from 'react';
import matchPath from './matchPath';
import RouterContext from './RouterContext';
export default class Switch extends PureComponent {
render() {
return <RouterContext.Consumer>
{
(context) => {
let match; // 標記是否匹配
let element; // 匹配到的元素
/**
* 這裡接受到的props.children有可能是一個也有可能是多個
* 理論上我們需要自行去做if判斷,但是React提供了一個api,React.Children
* 它當中的forEach會幫助我們完成這樣的事情
*/
React.Children.forEach(this.props.children, child => {
// isValidElement判斷是不是一個React節點
if (match == null && React.isValidElement(child)) {
element = child;
match = child.props.path
? matchPath(context.location.pathname, child.props)
: context.match
}
});
return match ? React.cloneElement(element, { computedMatch: mactch }) : null
}
}
</RouterContext.Consumer>
}
}
redirect
redirect是路由重定向,作用:
- 返回一個空元件。
- 跳轉到執行頁面
'use strict'
import React, { useEffect, PureComponent } from 'react';
import RouterContext from './RouterContext';
export default class Redirect extends PureComponent {
render() {
return <RouterContext.Consumer>
{
context => {
const { history } = context;
const { to } = this.props;
return <LifeCycle onMount={() => history.push(to)} />
}
}
</RouterContext.Consumer>
}
}
const LifeCycle = ({ onMount }) => {
useEffect(() => {
if (onMount) onMount()
}, [])
return null
}
常用的幾個hook
直接貼程式碼吧,這幾個簡單的我已經不會描述了。
import RouterContext from "./RouterContext";
import {useContext} from "react";
export function useHistory() {
return useContext(RouterContext).history;
}
export function useLocation() {
return useContext(RouterContext).location;
}
export function useRouteMatch() {
return useContext(RouterContext).match;
}
export function useParams() {
const match = useContext(RouterContext).match;
return match ? match.params : {};
}
withRouter就不寫了比較簡單,就是套個高階元件,然後獲取下context然後傳進去就行可以了。
總結
知識點基本都寫在前面了這裡做一個簡單總結:
- BrowserRouter元件在最上層決定路由體系使用什麼型別的history;
- 然後在Router檔案中定義context,使用跨層級通訊的方式傳遞history,match以及loaction等屬性,並使用history.listen監聽loaction的變化;
- 在Router元件和Switch元件中比對path和location,並渲染對應的元件,Switch元件決定渲染哪一個Route元件,而Route元件決定當前元件是否渲染;
- Route元件有三種渲染方式,互相是互斥的且
children
>component
>render
,需要注意是的三個屬性的入參標準,以及不建議component使用匿名函式方式入參; - Route裡還有一點需要注意就是為了讓我們後續使用中可以準確的獲取match,這裡在return的時候需要用
<RouterContext.Provider value={props}> </RouterContext.Provider>
包裹一次並傳入新的match,以及context的就近取值
特性; - Switch元件寓意為獨佔路由,也就是隻渲染匹配到的第一個Route元件;