react-router-dom源码揭秘
React Router中核心内容:
- router 组件(BrowserRouter)
- route matching 组件(Route,Switch,Redirect)
- navigation 组件(Link)
- hooks方法(useRouteMatch, useHistory, useLocation, useParams)
- 高阶组件(withRouter)
- 实用组件(Prompt)
背景
相信很多小伙伴和我一样,在学习react-router的时候会出现很多疑问,比如路由的三种渲染方式component, render,children以及他们的优先级,Redirect的重定向原理和Switch独占路由的实现…,今天我们一起来揭秘。今天这些实现只是做了个简单的处理,不考虑兼容性问题。
router 组件
BrowserRouter
import React, {Component} from "react";
import {createBrowserHistory} from "history";
import Router from "./Router"; // 这里暂不关注,但是这是BrowserRouter中的核心组件
export default class BrowserRouter extends Component {
constructor(props) {
super(props);
this.history = createBrowserHistory();
}
render() {
return <Router history={this.history} children={this.props.children} />;
}
}
复制代码
分析一下其实很简单,BrowserRouter中依赖了一个history库,他的主要作用就是传入history参数。使得children中的props中包含history对象。可以调用一些history上的方法,比如push,goBack,go,replace,listen….
补充一下history接口参数,方便大家知道history对象上不可以使用到的方法和参数。这里对泛型约束不做过多的解释,感兴趣的可以参考history库的github地址 github.com/ReactTraini…
interface History {
length: number;
action: 'PUSH' | 'POP' | 'REPLACE';
location: Location<HistoryLocationState>;
push(path: Path, state?: HistoryLocationState): void;
push(location: LocationDescriptor<HistoryLocationState>): void;
replace(path: Path, state?: HistoryLocationState): void;
replace(location: LocationDescriptor<HistoryLocationState>): void;
go(n: number): void;
goBack(): void;
goForward(): void;
block(prompt?: boolean | string | TransitionPromptHook<HistoryLocationState>): UnregisterCallback;
listen(listener: LocationListener<HistoryLocationState>): UnregisterCallback;
createHref(location: LocationDescriptorObject<HistoryLocationState>): Href;
}
复制代码
接着我们来说一下BrowserRouter中引用的Router组件,这是核心,在这里不考虑兼容性问题,使用React.createContext()来构建RouterContext。为了方便简单些,这里不构建HistoryContext。Router 组件会调用 history 的 listen 方法进行 路由监听,将监听到的 location 的值放在 RouterContext 中,location一旦更新, 子组件则会重新渲染。这就是为什么一定得通过coontext将值传递下去的主要原因。computeRootMatch这个方法,是保证path不存在的时候,也能返回一个参数对象。相当于一个默认值。
import React, {Component} from "react";
const RouterContext = React.createContext()
export default class Router extends Component {
static computeRootMatch(pathname) { //路由匹配方法
return {path: "/", url: "/", params: {}, isExact: pathname === "/"};
}
constructor(props) {
super(props);
this.state = {
location: props.history.location
};
// location发生变化,要执行这里的回调
this.unlisten = props.history.listen(location => {
this.setState({location});
});
}
componentWillUnmount() {
if (this.unlisten) {
this.unlisten();
}
}
render() {
return (
<RouterContext.Provider
value={{
history: this.props.history,
location: this.state.location,
// path url params isExact 四个属性
match: Router.computeRootMatch(this.state.location.pathname)
}}>
{this.props.children}
</RouterContext.Provider>
);
}
}
复制代码
函数式组件的实现:
import React, { useState, useEffect } from "react";
import { RouterContext } from "./Context"; // 等价于 const RouterContext = React.createContext() ,但是尽量把RouterContext放在一个目录下,方便其他组件消费。
export default function Router(props) {
const [location, setLocation] = useState(props?.history?.location);
useEffect(() => {
// setLocation()
// location发生变化,要执行这里的回调
const unlisten = props.history.listen((location) => {
setLocation(location);
});
return () => {
if (unlisten) {
unlisten();
}
};
}, [props.history]);
const computeRootMatch = (pathname) => {
return { path: "/", url: "/", params: {}, isExact: pathname === "/" };
};
return (
<RouterContext.Provider
value={{
history: props.history,
location: location,
// path url params isExact 四个属性
match: computeRootMatch(location.pathname),
}}
>
{props.children}
</RouterContext.Provider>
);
}
复制代码
route matching 组件
Route
Route组件应该是react-router-dom的核心组件了,这里面包含了路由路径匹配的实现,以及路由渲染的逻辑,在这里,你会明白路由的三种渲染方式的优先级,以及他们是做了什么处理才能实现路由的匹配和children组件的方式是否匹配都会渲染的问题。现在让我们一一来剖析。
首先matchPath这个方法主要做的是去判断路由是否匹配的功能。函数返回值的列表如下:
return {
path, // the path used to match
url: path === "/" && url === "" ? "/" : url, // the matched portion of the URL
isExact, // whether or not we matched exactly
params: keys.reduce((memo, key, index) => {
memo[key.name] = values[index];
return memo;
}, {})
};
复制代码
请看下面代码,match参数的计算是首先判断是否存在computeMatch,这个props参数主要是为了实现独占路由,如果存在这个,则取computeMatch,这个computeMatch是Switch中传递下来的,主要是破坏Route中原来的匹配规则,使得不匹配时,就算传递children也不渲染。具体在哪请看Switch的实现。如果没有这个则去path,path是Route中的参数,如果有则通过matchPath计算出来match,没有的话则取默认的match,上面Router上写了一个这个的静态的方法computeRootMatch,主要是在这里起作用。
const {children, component, render, path, computedMatch} = this.props;
const match = computedMatch
? computedMatch
: path
?matchPath(location.pathname, this.props)
: context.match;
复制代码
接下来的核心是组件的三种渲染方式的实现,渲染优先级children>component>render。看了源码你就会明白为啥是这样。首先match匹配的情况是存在则首先判断是否存在children,如果children是函数则children(props),否则渲染children组件,同理判断component和render。match不匹配的时候,判断children,在这里你就会明白如果就是路由不匹配,如果存在children组件也会被渲染。
<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>
复制代码
import React, {Component} from "react";
import matchPath from "./matchPath";
import {RouterContext} from "./Context";
export default class Route extends Component {
render() {
return (
<RouterContext.Consumer>
{context => {
const location = context.location;
const {children, component, render, path, computedMatch} = this.props;
const match = computedMatch
? computedMatch
: path
? matchPath(location.pathname, this.props)
: context.match;
const props = {
...context,
match
};
// match children, component, render, null
// 不match children(function), null
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>
);
}
}
复制代码
Link
Link 组件 内部实现也是很简单的,主要是a标签,然后禁止a链接的默认跳转的行为。Link组件身上有个很重要的属性to,这个属性表示点击后要跳转的路由。此外这里使用了context,主要是接收到history对象。class组件和函数式组件实现代码如下:
import React, {Component,useContext} from "react";
import {RouterContext} from "./Context";
export default class Link extends Component {
static contextType = RouterContext;
handleClick = event => {
event.preventDefault();
// 事件做跳转
this.context.history.push(this.props.to);
};
render() {
const {to, children, ...otherProps} = this.props;
return (
<a href={to} {...otherProps} onClick={this.handleClick}>
{children}
</a>
);
}
}
复制代码
函数式组件实现
export default function Link(props) {
const context = useContext(RouterContext);
const { to, children, ...restProps } = props;
const handleClick = (event) => {
event.preventDefault();
// 事件做跳转
context.history.push(to);
};
return (
<a href={to} {...restProps} onClick={handleClick}>
{children}
</a>
);
}
复制代码
Switch
Switch是独占路由,在路由匹配过程中只能匹配第一个与之匹配的路由组件。核心就是去遍历Switch下的children组件,去找寻与之匹配路由。核心函数是React.Children.forEach,React.Children.forEach(children, function[(thisArg)])
在 children 里的每个直接子节点上调用一个函数,并将 this 设置为 thisArg。如果 children 是一个数组,它将被遍历并为数组中的每个子节点调用该函数。注意如果 children 是一个 Fragment 对象,它将被视为单一子节点的情况处理,而不会被遍历。React.cloneElement(element, {computedMatch: match})
,上面谈论到的Route中的computedMatch方法就是这里传递下去的,为的就是破坏Route中的匹配规则,这样才能实现独占路由。React.cloneElement(element,[props],[...children])
以 element 元素为样板克隆并返回新的 React 元素。返回元素的 props 是将新的 props 与原始元素的 props 浅层合并后的结果。新的子元素将取代现有的子元素,而来自原始元素的 key 和 ref 将被保留。
React.isValidElementType
方法用于判断目标是不是一个有效的 React 元素类型,以下类型会被认为是有效的:string、function、ReactSymbol
import React, {Component} from "react";
import {RouterContext} from "./Context";
import matchPath from "./matchPath";
export default class Switch extends Component {
render() {
return (
<RouterContext.Consumer>
{context => {
//match 是否匹配
//element 记录匹配的元素
const {location} = context;
let match, element;
React.Children.forEach(this.props.children, child => {
if (match == null && React.isValidElement(child)) {
element = child;
const {path} = child.props;
match = path
? matchPath(location.pathname, child.props)
: context.match;
}
});
return match
? React.cloneElement(element, {
computedMatch: match
})
: null;
}}
</RouterContext.Consumer>
);
}
}
复制代码
Redirect
Redirect是路由重定向,主要是重定向到指定的路由,里面有个to属性,代表要重定向要指定的路由。LifeCycle
组件是实现路由跳转逻辑的功能,class组件render过程无法在指定的生命周期中做逻辑,故而借助这个中间组件。
import React, {Component} from "react";
import {RouterContext} from "./Context";
export default class Redirect extends Component {
render() {
return (
<RouterContext.Consumer>
{context => {
const {history} = context;
const {to, push = false} = this.props;
return (
<LifeCycle
onMount={() => {
push ? history.push(to) : history.replace(to);
}}
/>
);
}}
</RouterContext.Consumer>
);
}
}
class LifeCycle extends Component {
componentDidMount() {
if (this.props.onMount) {
this.props.onMount.call(this, this);
}
}
render() {
return null;
}
}
复制代码
hooks方法
hooks方法实现起来就简单很多,话不多说直接撸代码,相信你一看就明白。
import React, {useContext} from "react";
import {RouterContext} from "./Context";
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 : {};
}
复制代码
是不是很简单呢。当然你会问如果是class组件,用不了hooks,那这些参数方法该怎么获取呢,不用着急,其实你知道的,没错就是高阶组件withRouter。
withRouter
withRouter就是用来获取params、location、history、routeMatch
,看代码,实现起来是不是很简单。
import React from "react";
import {RouterContext} from "./Context";
const withRouter = WrappedComponent => props => {
return (
<RouterContext.Consumer>
{context => {
return <WrappedComponent {...props} {...context} />;
}}
</RouterContext.Consumer>
);
};
export default withRouter;
复制代码
实用组件
当你想离开页面时都会弹出一个提示框(alert),让你选择是否残忍离开。React路由也为我们准备了这样的组件,就是prompt。
标签有两个属性:
message
:用于显示提示的文本信息。
when
:传递布尔值,相当于标签的开关,默认是 true,设置成 false 时,失效。
import React from "react";
import { RouterContext } from "./Context";
export default function Prompt({ message, when = true }) {
return (
<RouterContext.Consumer>
{(context) => {
if (!when) {
return null;
}
let method = context.history.block;
return (
<LifeCycle
onMount={(self) => {
self.release = method(message);
}}
onUnMount={(self)=>{
self.release();
}}
></LifeCycle>
);
}}
</RouterContext.Consumer>
);
}
class LifeCycle extends React.Component {
componentDidMount() {
if (this.props.onMount) {
this.props.onMount.call(this, this);
}
}
componentWillUnmount() {
if (this.props.onUnMount) {
this.props.onUnMount.call(this, this);
}
}
render() {
return null;
}
}
复制代码
到这里,就该结束了,当然还有一个NavLink组件,不过你会了Link的实现,这个也是很好实现的,它只是在Link上接了一些属性,这里就不再重复了。你有没有很好的理解react-router-dom呢,其实它内部的核心实现就这些,只不过它比较严谨,还做了一些环境的判断和一些warning和错误处理。最后放上git地址欢迎查看完整的代码。