前言
花了一点时间把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>
}
}
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组件;