前言花了一点时间梳理了react-router系统,包括功能原理和常用组件的基本实现,文中贴出的代码是每个组件的实现的核心原理,会和源码略有不同。请注意,源码地址都提供了详细链接,可以点击跳转。放心吃。渲染方式childrencomponentrender优先级:这三种渲染方式是互斥的,如果同时存在则为:children>component>render;这是源代码中关于优先级部分的代码;注意children和render只能作为匿名函数以节点的形式显示,组件不需要。component和render只有path匹配后才能显示,children不管匹配不匹配都会显示。Component不建议以匿名函数的形式传入要显示的节点,因为渲染时会调用React.createElement。如果使用匿名函数的形式,每次都会生成一个新的类型,导致子组件频繁挂载和卸载的问题,children和render不会;有兴趣的可以尝试运行代码;'使用严格';从“反应”中导入反应,{useState,useEffect};从'react-router'导入{Router,Route};constChild=(props)=>{useEffect(()=>{console.log("mount");return()=>console.log("unmount");},[]);返回
Child-{props.count}
}classChildFuncextendsReact.Component{componentDidMount(){console.log("componentDidMount");}componentWillUnmount(){console.log("componentWillUnmount");}render(){return
ChildFunc-{this.props.count}
}}constIndex=(props)=>{const[count,setCount]=useState(0);return
setCount((state)=>state+1)}>addchickchangecount{count}
{/*bad观察挂载和卸载函数的日志*/}}/>}/>{/*好这是正确打开方式是观察挂载和卸载函数的日志*/}}/>}/>{/*观察mount和unmount函数的log,这个也是可以的,但是children不需要匹配路径,慎用*/}}/>}/> };导出默认索引;Link组件link本质上是一个a标签,但是直接使用href属性点击时会出现抖动,需要使用命令跳转。源码中给它添加了一些属性和函数,处理to和click事件的参数。源码请移至'usestrict';importReact,{useContext}from'react'importRouterContextfrom'./RouterContext'exportdefaultfunctionLink({to,children}){const{history}=useContext(RouterContext)consthandle=e=>{//防止抖动,因此禁用默认行为命令形式jumpe.preventDefault();history.push(to)};return
{children}/a>};BrowserRouter组件该组件是react-router的顶层组件,主要作用是判断路由系统选择哪个路由使用。查看源码请移至'usestrict'importReact,{PureComponent}from'react';import{createBrowserHistory}from'history'importRouterfrom"./Router"exportdefaultclassBrowserRouterextendsPureComponent{constructor(props){超级(道具);this.history=createBrowserHistory();}render(){return{this.props.children}}};RouterContext.js文件可以用普通元素节点的嵌套不能很好的确定具体的层级关系,所以我们还是选择数据跨层的方式来实现。声明和导出RouterContext拆分成单独的文件将使逻辑更清晰。源码中并没有直接使用createContext,而是包裹了一层createNamedContext,为生成的context添加一个displayName。源代码“严格使用”;从'react'导入ReactconstRouterContext=React.createContext();导出默认RouterContext;路由器。js文件Router文件主要作用:通过RouterContext向下传递历史、位置、匹配等属性;通过history.listen监听页面的location变化,并将location向下传递,方便Route组件和Switch组件的匹配;源代码'usestrict'importReact,{PureComponent}from'react';importRouterContextfrom'RouterContext'exportdefaultclassRouterextendsPureComponent{staticcomputeRootMatch(pathname){return{path:"/",url:"/",参数:{},isExact:路径名===“/”};}constructor(){super(props)this.state={location:props.history.location}this.unlinsten=props.history.listen((location)=>{this.setState({location})})}componentWillUnmount(){this.unlinsten();}render(){return({this.props.children})}}路由组件路由组件主要负责处理的匹配并返回需要渲染的组件组件。这个匹配可以是上层Switch组件传下来的computedMatch。如果上层没有使用Switch组件,则判断Route组件接收到的path属性是否存在。然后与location.pathname进行比较,如果匹配则不显示,则不显示。路径也可以为空。如果为空,直接使用context.match;源代码matchPath源代码'usestrict'importReact,{PureComponent}from'react';从'./matchPath'导入matchPath;从'./RouterContext'导入RouterContext;导出默认类RouteextendsPureComponent{render(){return{(context)=>{const{path,children,component,render,computedMatch}=this.props;const{位置}=上下文;//match时,表示当前匹配成功constmatch=computedMatch?计算匹配:路径?matchPath(location.pathname,this.props):context.match;constprops={...context,match}//匹配成功后,应该按照children的优先级进行渲染>component>renderreturn{match?孩子们?typeofchildren===“函数”?儿童(道具):儿童:组件?React.createElement(component,props):渲染?render(props):null:typeofchildren==="function"?孩子(道具):空}}}}}注意:上面代码中反复提到的匹配是我们路由挂载参数的匹配;我们在组件的返回值中包裹了一层RouterContext。Provider,原因是我们在外部使用useRouteMatch和useParams获取match的时候,context获取的match其实是Router传过来的初始值。Layer,这里是使用上下文的最近值;switch组件Switch表示独占路由,作用:匹配路由,只渲染第一个匹配到的路由或重定向;因为上面的原因,比如404,不要写path属性,组件一定要放在最后,否则一旦匹配到404组件,后面的子组件就不会再匹配了;与Route组件的区别在于Switch控制显示显示哪个Route组件,Route组件为空是当前Route组件下的组件是否显示源码'usestrict'importReact,{PureComponent}from'react';importmatchPathfrom'./matchPath';import来自“./RouterContext”的RouterContext;导出默认类SwitchextendsPureComponent{render(){return{(context)=>{让匹配;//标签是否匹配let元素;//匹配的元素/***这里接收到的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}});回归比赛?反应克隆eElement(element,{computedMatch:mactch}):null}}}}redirectredirect为路由重定向,作用:返回一个空组件跳转到执行页面源代码'usestrict'importReact,{useEffect,PureComponent}来自'react';从'./RouterContext'导入RouterContext;导出默认类RedirectextendsPureComponent{render(){return{context=>{const{history}=context;const{to}=this.props;返回history.push(to)}/>}}}}constLifeCycle=({onMount})=>{useEffect(()=>{if(onMount)onMount()},[])returnnull}直接贴几个常用hook的代码。我不会描述这些简单的。从“./RouterContext”导入RouterContext;从“react”导入{useContext};导出函数useHistory(){returnuseContext(RouterContext).history;}exportfunctionuseLocation(){returnuseContext(RouterContext).location;}exportfunctionuseRouteMatch(){returnuseContext(RouterContext).match;}导出函数useParams(){constmatch=useContext(RouterContext).match;回归比赛?match.params:{};}WithRouter就不写了,比较简单,就是设置一个高阶组件,然后获取上下文传入即可。总结知识点基本都写在前面了。这里有一个简短的总结:BrowserRouter组件决定路由系统在顶层使用什么类型的历史记录;然后在Router文件中定义context,使用跨层通信传递history、match和loaction等属性,使用history.listen监听loaction变化;比较Router组件和Switch组件中的路径和位置,渲染对应的组件,Switch组件决定渲染哪个Route组件,Route组件决定是否渲染当前组件;RouteComponents有三种渲染方式,它们是互斥的,children>component>render。需要注意三个属性的入参标准,不建议组件使用匿名函数入参;Route还有一点需要注意。让我们在后续的使用中准确的得到匹配。这里需要用包裹一次,返回时传入新的匹配和上下文的最近值特征;Switch组件表示独占路由,即是,只渲染第一个匹配的Route组件;