React router dom v6 从浅到浅
前言
当面试官问你,React router
原理是什么,很多同学可能脱口而出 history、hash
,但这就足够了么?相信面试官肯定想听到更多理解,本文的浅分析,分成三大块,html5 history api
、History库
以及重点 React router dom库
,您可以了解到:
- 为何
React-react
包内有三个不同模块 - 如何监听路由变化并切换相应组件
- 嵌套路由是如何实现的
本文纯属自己兴趣在探究原理的过程中记录下的,有不对地方请多包涵。当然有帮助的话不要吝啬您的👍
阅读前准备
阅读本文前最好能够先把源码下下来对照分析,会加深印象
git clone https://github.com/ReactTraining/history.git
git clone https://github.com/ReactTraining/react-router.git (记得切换分支到 v6.0 版本)
HTML5 - History API
History
是 HTML5 新出的API,允许操作浏览器的曾经在标签页或者框架里访问的会话历史记录。主要特性是可以在不刷新整个页面的情况下修改站点的URL。像 vue-router
、react-router-dom
都是基于这个特性来实现路由的跳转的。
直接在控制台上打印 window.history
看看:
主要讲解常用的几个属性以及 API
:
History.length
:返回一个整数,表示会话历史中元素的数目,包括当前加载的页
History.state
:返回一个表示历史堆栈顶部的状态的值
History.back()
:前往上一页,即返回按钮,等价于 history.go(-1)
History.go()
:通过当前页面的相对位置从浏览器历史记录( 会话记录 )加载页面
History.pushState()
:按指定的名称和URL(如果提供该参数)将数据push进会话历史栈
History.replaceState()
:按指定的数据,名称和URL(如果提供该参数),更新历史栈上最新的入口
注意:
pushState
和replaceState
主要区别在于,前者是会往历史记录栈顶加一条记录,而后者是直接替换当前的栈记录。比如当在登录页/login
登录成功后进行跳转时,一般使用repalceState
直接替换掉 login 这条记录,这样防止点击后退时进入登录页又直接跳转回主页的情况。
这里还有个事件可以注意下:popstate
,当活动历史记录条目更改时,将触发 popstate
事件。当调用 pushState
或 replaceState
时不会触发 onPopstate
事件,只有在调用了 back()
或者 forward()
方法才会触发。
更多相关知识可以查看 MDN介绍
History库
这里主要是针对 history@5.0.0
版本进行分析。5.0版本是基于 ts
进行重构的,所以对 ts
不熟悉的同学可以简单去过下 ts
的基础知识,但其实理解上也差不多通用。
History
库基于浏览器history api
上封装了下,提供了一个包含 URLs
和 state
的location
对象,追踪浏览器的历史记录。下面分析下源码。
由于这里主要针对浏览器的 history
进行解析的,所以着重看 createBrowserHistory
这个方法,在代码的第 397 ~ 600 行,内容不多。
首先先来看下 createBrowserHistory
返回了什么内容(第 559 ~ 600 行)
这里我们主要讲两个,一个是
location
,另一个是listen
,这两个是比较核心的物件。其他api像go()
,back()
跟原生的方法基本相同,有兴趣可以自己琢磨下
createBrowserHistory
- 🤠 第 397 ~ 401 行
比较容易理解,解构传过来的 options
的值,如果没有 window
参数,则默认取当前窗口对象所关联的 window
对象。然后把 history
存放到一个变量里
- 第 403 ~ 416 行
通过 window.location
获取当前的路由位置信息并解构,然后返回一个由 history state
的 idx
属性以及 location
对象组合的数组。继续往下看
- 🤠 第 419 ~ 463 行
这里我省略了一些代码,那块主要是处理路由跳转前的一个确认操作,先不做分析。此处主要监听了一个事件 popState
,这个事件开头也说过了,当调用 back()
或者是 forward()
才会触发。来看下这个事件做了哪些操作。定义一个变量 nextAction
为 Action.Pop
也就是 pop
。然后调用 applyTx()
方法。下面继续看这个方法做了什么操作。
- 🤠 第 508 ~ 512 行
接收一个参数 nextAction
,然后调用 getIndexAndLocation()
方法, 此方法我们上面讲过,主要是获取当前路由的相关信息,即 location
对象。然后调用了一个 listeners.call()
将 action
和 location
传入。继续来看 listeners
是何方神圣。
- 🤠 第 465 ~ 468 以及 1048 ~ 1065 行
来看 listeners
的定义,是由一个 createEvents()
返回的。这个方法,顾名思义,就是创建事件。定义了一个变量 handlers
数组,用于存放要处理的回调函数事件。然后返回了一个对象。push
方法就是往 handlers
中添加要执行的函数。这块主要在 history.listen()
中使用,可以翻到开头看下 history
中返回了 listen()
方法,就是调用了 listeners.push(listener)
。最后 call()
方法就比较容易理解,就是取出 handlers
里面的回调函数并逐个执行。
梳理
按照上面一步一步讲下来,肯定有同学又忘了上一步是干啥的了。这里简单用图把所有流程串起来,梳理清楚。
history
导出了名为createBrowserHistory
的方法,其中监听了popState
事件- 当触发
popState
事件后,调用getIndexAndLocation
获取当前最新的路由对象信息并更新location
的值 - 取出放在
listeners
里面的回调函数,并循环执行,传入当前的action
和location
作为回调函数的参数
从这里可以看出 react-router
能够得知路由的变化,主要靠的就是 history.listen
这个方法,将路由变化时要执行的回调函数传入进去。下节开始分析重头戏 react-router-dom
是如何结合 History
库应用的。
React-router-dom
注意:以下源码是基于
v6.0.0
版本的。
下面开始分析 React-router-dom
,直接查看源码包,可以发现有三个文件夹
react-router
:这是实现路由的核心代码react-router-dom
:这是为浏览器dom专用,新增了如createBrowserHistory
等react-router-native
:这是为 RN 准备的路由相关库
v6版本基本用法
直接先来看在 v6 版本中一个路由该如何写
可以看到,最外层仍然是用 BrowserRouter
包裹着路由,只不过 Switch
换成了 Routes
,Routes
组件是整个路由中最核心的,决定了当前路径应匹配显示那个页面组件。这里还有个点不一样,对于嵌套路由,比如示例中当路径是 users/me
时,会先匹配到 Users
组件,Users
组件里可以看到有个 <Outlet />
组件,相当于占位符,二级路径 me
匹配到了 <OwnUserProfile>
组件,填充进 Outlet
,从而实现路由的嵌套。下面看下每个组件的内容
BrowserRouter
🤠react-router-dom 第 92 ~ 118 行
前几行,使用 useRef
定义了一个可变的对象,调用了 History
库的 createBrowserHistory
方法,并存放到 ref
里去。这样可以达到变量 history
对象中取的永远是最新的值
接下来,定义了一个 useReducer
,初始的 state
是从 history
对象中取出 action
以及 location
的值。具体 dispatch
做了什么,等下再说,先往下看
调用了 history.listen()
并把上面返回的 dispatch
方法作为回调传进去。具体 listen
方法做了什么上面讲过了,忘了的同学可以翻回去看下。当dispatch
被调用时会接收一个参数 { action, location }
,这个参数会传递到 reducer
里面的 action
里去,然后直接返回 action
,从而更新了 state
的值。注意要区分history里的action 和 reducer里的action,这段 reducer
就比较好理解了。
最后返回 <Router>
组件,紧跟着找下这个组件。
🤠react-router 第 281 ~ 300 行
比较容易理解,接收参数,并返回一个 Context.Provider
,传递数据给下面的子组件。
Route
🤠react-router 第 249 ~ 253 行
我们先跳过 Routes
,直接先看 Route
,很简单,就直接返回 element
里的内容
Routes
下面看下 Routes
,作为路由里面最核心的部位,内容比较多,分析过程中可能会省略一些不重要的东西。
🤠react-router 第 333 ~ 340 行
实际就两行,调用 createRoutesFromChildren
方法,这个方法就不具体讲了。主要是利用 React.Children
将子组件里的内容提取出来,返回一个数组。格式如下图所示:
最后返回 useRoutes_
方法的结果。
useRoutes_
🤠react-router 第 582 ~ 639 行
这个方法比较复杂,所以分析时可能会省略一些。整体看下,该方法调用了 matchRoutes
匹配出和当前路径相符和的列表,然后直接返回渲染对应的组件。因此重点是 matchRoutes
这个方法
🤠react-router 第 768 ~ 798 行
从 location
对象中获取 pathname
,并调用 flattenRoutes
将数组打平(该方法不做说明,可以去源码看下实现内容),branches
内容如下:
可见,branches
中的每项由三部分组成,最主要是前两个,第一个是 路径 ,第二个则是包含的 route
对象,比如 users/me
是由两部分组成的,因此 route
数组中就包含了这两个路径的属性值。接下来的 rankRouteBranches
是根据路径来进行排序,比如通配符,路径长短等。具体可以看下实现方式,不详细赘述。
接下来遍历了 branches
数组,调用 matchRouteBranch
进行一一匹配。看下 matchRouteBranch
方法。
matchRouteBranch
🤠react-router 第 904 ~ 941 行
这部分能说是路由匹配的核心算法,取上部分每一项 branch
进行匹配。比如还是按照上面那个例子,当前路径是 /users/me
,一开始 remainingPathname
初始值就是要匹配的路径 /users/me
,调用 matchPath
进行匹配判断,匹配成功返回匹配到的路径即 /users
,放到 matches
数组里面。下个循环,remainingPathname
则变成了 /me
,去掉了已经匹配到的 /users
部分,后面操作同样。最终 matches
数组如下(此处暂不考虑路径带有参数的情况):
这部分路由匹配可能比较繁杂,建议可以截取源码部分放到控制台上,自己模拟数据运行 debugger,一步一步走,可能会更加清晰。
最后,回头看下一开始的 useRoutes_
方法返回了什么
分析上面代码时先看看 outlet
做什么,这个在前面已经讲过它的作用,相当于子路由的占位符
可以看到,其实就是使用 useContext
获取最近的 RouteContext
中的 outlet
的值。
因此,使用 reduceRighr
从右侧即从子路由的组件开始遍历,children
理所当然是 element
里的组件内容。每次循环 outlet
的值都会等于其中 return
的值,总的来说,有点像 千层饼,层层嵌套。类似下面:
<RouteContext.Provider
value={{
outlet // 嵌套
|
|----<RouteContext.Provider
value={{
outlet // 嵌套
|
|----<RouteContext.Provider
/>
这样 outlet
占位符取到的就是离它最近的父组件提供的 value
的值,渲染对应的组件。
总结
由于本篇只是针对
react-router-dom
的原理进行简单分析,所以略过了一些路由匹配边界的判断以及携带参数时的处理方法。
看完想必有些同学可能还是懵懵懂懂,借用一张图来概括下整个流程
- 首先,
<Router>
元件用history.location
初始化location
状态 <Route>
元件会从 Context 中拿到location
,然后渲染符合location
的元件- 当使用者点击 UI 上的
<Link>
时,会呼叫history.push()
把定义在<Route>
上的to
放进 history 中,这时浏览器的 URL 会跟着一起变动,但不会跳转页面 - 接着,位置的改动会触发
history.listen()
,调用useReducer
中的dispatch
,进而改变原本存储在<Router>
中的location
状态,重复以上第二步骤
Tips: 如果本文有错误欢迎各位同学指出,谢谢啦😉