React router dom v6 从浅到浅

时间:2020-9-10 作者:admin

React router dom v6 从浅到浅

前言

当面试官问你,React router原理是什么,很多同学可能脱口而出 history、hash,但这就足够了么?相信面试官肯定想听到更多理解,本文的浅分析,分成三大块,html5 history apiHistory库以及重点 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-routerreact-router-dom都是基于这个特性来实现路由的跳转的。

直接在控制台上打印 window.history看看:

React router dom v6 从浅到浅

主要讲解常用的几个属性以及 API

History.length:返回一个整数,表示会话历史中元素的数目,包括当前加载的页

History.state:返回一个表示历史堆栈顶部的状态的值

History.back():前往上一页,即返回按钮,等价于 history.go(-1)

History.go():通过当前页面的相对位置从浏览器历史记录( 会话记录 )加载页面

History.pushState():按指定的名称和URL(如果提供该参数)将数据push进会话历史栈

History.replaceState():按指定的数据,名称和URL(如果提供该参数),更新历史栈上最新的入口

注意:pushStatereplaceState主要区别在于,前者是会往历史记录栈顶加一条记录,而后者是直接替换当前的栈记录。比如当在登录页 /login登录成功后进行跳转时,一般使用 repalceState直接替换掉 login 这条记录,这样防止点击后退时进入登录页又直接跳转回主页的情况。

这里还有个事件可以注意下:popstate,当活动历史记录条目更改时,将触发 popstate事件。当调用 pushStatereplaceState时不会触发 onPopstate事件,只有在调用了 back()或者 forward()方法才会触发。

更多相关知识可以查看 MDN介绍

History库

这里主要是针对 history@5.0.0版本进行分析。5.0版本是基于 ts进行重构的,所以对 ts不熟悉的同学可以简单去过下 ts的基础知识,但其实理解上也差不多通用。

History库基于浏览器history api上封装了下,提供了一个包含 URLsstatelocation对象,追踪浏览器的历史记录。下面分析下源码。

由于这里主要针对浏览器的 history进行解析的,所以着重看 createBrowserHistory这个方法,在代码的第 397 ~ 600行,内容不多。

首先先来看下 createBrowserHistory返回了什么内容(第 559 ~ 600行)

React router dom v6 从浅到浅

这里我们主要讲两个,一个是 location,另一个是 listen,这两个是比较核心的物件。其他api像 go()back()跟原生的方法基本相同,有兴趣可以自己琢磨下

createBrowserHistory

  • 🤠 第 397 ~ 401

React router dom v6 从浅到浅

比较容易理解,解构传过来的 options的值,如果没有 window参数,则默认取当前窗口对象所关联的 window对象。然后把 history存放到一个变量里

  • 403 ~ 416

React router dom v6 从浅到浅

通过 window.location获取当前的路由位置信息并解构,然后返回一个由 history stateidx属性以及 location对象组合的数组。继续往下看

  • 🤠 第 419 ~ 463

React router dom v6 从浅到浅

这里我省略了一些代码,那块主要是处理路由跳转前的一个确认操作,先不做分析。此处主要监听了一个事件 popState,这个事件开头也说过了,当调用 back()或者是 forward()才会触发。来看下这个事件做了哪些操作。定义一个变量 nextActionAction.Pop也就是 pop。然后调用 applyTx()方法。下面继续看这个方法做了什么操作。

  • 🤠 第 508 ~ 512

React router dom v6 从浅到浅

接收一个参数 nextAction,然后调用 getIndexAndLocation()方法, 此方法我们上面讲过,主要是获取当前路由的相关信息,即 location对象。然后调用了一个 listeners.call()actionlocation传入。继续来看 listeners是何方神圣。

  • 🤠 第 465 ~ 468以及 1048 ~ 1065

React router dom v6 从浅到浅

React router dom v6 从浅到浅

来看 listeners的定义,是由一个 createEvents()返回的。这个方法,顾名思义,就是创建事件。定义了一个变量 handlers数组,用于存放要处理的回调函数事件。然后返回了一个对象。push方法就是往 handlers中添加要执行的函数。这块主要在 history.listen()中使用,可以翻到开头看下 history中返回了 listen()方法,就是调用了 listeners.push(listener)。最后 call()方法就比较容易理解,就是取出 handlers里面的回调函数并逐个执行。

梳理

按照上面一步一步讲下来,肯定有同学又忘了上一步是干啥的了。这里简单用图把所有流程串起来,梳理清楚。

React router dom v6 从浅到浅

  • history导出了名为 createBrowserHistory的方法,其中监听了 popState事件
  • 当触发 popState事件后,调用 getIndexAndLocation获取当前最新的路由对象信息并更新 location的值
  • 取出放在 listeners里面的回调函数,并循环执行,传入当前的 actionlocation作为回调函数的参数

从这里可以看出 react-router能够得知路由的变化,主要靠的就是 history.listen这个方法,将路由变化时要执行的回调函数传入进去。下节开始分析重头戏 react-router-dom是如何结合 History库应用的。

React-router-dom

注意:以下源码是基于 v6.0.0版本的。

下面开始分析 React-router-dom,直接查看源码包,可以发现有三个文件夹

React router dom v6 从浅到浅

  • react-router:这是实现路由的核心代码
  • react-router-dom:这是为浏览器dom专用,新增了如 createBrowserHistory
  • react-router-native:这是为 RN 准备的路由相关库

v6版本基本用法

直接先来看在 v6 版本中一个路由该如何写

React router dom v6 从浅到浅

可以看到,最外层仍然是用 BrowserRouter包裹着路由,只不过 Switch换成了 RoutesRoutes组件是整个路由中最核心的,决定了当前路径应匹配显示那个页面组件。这里还有个点不一样,对于嵌套路由,比如示例中当路径是 users/me时,会先匹配到 Users组件,Users组件里可以看到有个 <Outlet />组件,相当于占位符,二级路径 me匹配到了 <OwnUserProfile>组件,填充进 Outlet,从而实现路由的嵌套。下面看下每个组件的内容

BrowserRouter

🤠react-router-dom 第 92 ~ 118 行

React router dom v6 从浅到浅

前几行,使用 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 行

React router dom v6 从浅到浅

比较容易理解,接收参数,并返回一个 Context.Provider,传递数据给下面的子组件。

Route

🤠react-router 第 249 ~ 253 行

React router dom v6 从浅到浅

我们先跳过 Routes,直接先看 Route,很简单,就直接返回 element里的内容

Routes

下面看下 Routes,作为路由里面最核心的部位,内容比较多,分析过程中可能会省略一些不重要的东西。

🤠react-router 第 333 ~ 340 行

React router dom v6 从浅到浅

实际就两行,调用 createRoutesFromChildren方法,这个方法就不具体讲了。主要是利用 React.Children将子组件里的内容提取出来,返回一个数组。格式如下图所示:

React router dom v6 从浅到浅

最后返回 useRoutes_方法的结果。

useRoutes_

🤠react-router 第 582 ~ 639 行

React router dom v6 从浅到浅

这个方法比较复杂,所以分析时可能会省略一些。整体看下,该方法调用了 matchRoutes匹配出和当前路径相符和的列表,然后直接返回渲染对应的组件。因此重点是 matchRoutes这个方法

🤠react-router 第 768 ~ 798 行

React router dom v6 从浅到浅

location对象中获取 pathname,并调用 flattenRoutes将数组打平(该方法不做说明,可以去源码看下实现内容),branches内容如下:

React router dom v6 从浅到浅

可见,branches中的每项由三部分组成,最主要是前两个,第一个是 路径,第二个则是包含的 route对象,比如 users/me是由两部分组成的,因此 route数组中就包含了这两个路径的属性值。接下来的 rankRouteBranches是根据路径来进行排序,比如通配符,路径长短等。具体可以看下实现方式,不详细赘述。

接下来遍历了 branches数组,调用 matchRouteBranch进行一一匹配。看下 matchRouteBranch方法。

matchRouteBranch

🤠react-router 第 904 ~ 941 行

React router dom v6 从浅到浅

这部分能说是路由匹配的核心算法,取上部分每一项 branch进行匹配。比如还是按照上面那个例子,当前路径是 /users/me,一开始 remainingPathname初始值就是要匹配的路径 /users/me,调用 matchPath进行匹配判断,匹配成功返回匹配到的路径即 /users,放到 matches数组里面。下个循环,remainingPathname则变成了 /me,去掉了已经匹配到的 /users部分,后面操作同样。最终 matches数组如下(此处暂不考虑路径带有参数的情况):

React router dom v6 从浅到浅

这部分路由匹配可能比较繁杂,建议可以截取源码部分放到控制台上,自己模拟数据运行 debugger,一步一步走,可能会更加清晰。

最后,回头看下一开始的 useRoutes_方法返回了什么

React router dom v6 从浅到浅

分析上面代码时先看看 outlet做什么,这个在前面已经讲过它的作用,相当于子路由的占位符

React router dom v6 从浅到浅

可以看到,其实就是使用 useContext获取最近的 RouteContext中的 outlet的值。

因此,使用 reduceRighr从右侧即从子路由的组件开始遍历,children理所当然是 element里的组件内容。每次循环 outlet的值都会等于其中 return的值,总的来说,有点像 千层饼,层层嵌套。类似下面:

<RouteContext.Provider 
    value={{
        outlet // 嵌套
           |
           |----<RouteContext.Provider 
                        value={{
                            outlet // 嵌套
                                |
                                |----<RouteContext.Provider 
/>

这样 outlet占位符取到的就是离它最近的父组件提供的 value的值,渲染对应的组件。

总结

由于本篇只是针对 react-router-dom的原理进行简单分析,所以略过了一些路由匹配边界的判断以及携带参数时的处理方法。

看完想必有些同学可能还是懵懵懂懂,借用一张图来概括下整个流程

React router dom v6 从浅到浅

  1. 首先,<Router>元件用 history.location初始化 location状态
  2. <Route>元件会从 Context 中拿到 location,然后渲染符合 location的元件
  3. 当使用者点击 UI 上的 <Link>时,会呼叫 history.push()把定义在 <Route>上的 to放进 history 中,这时浏览器的 URL 会跟着一起变动,但不会跳转页面
  4. 接着,位置的改动会触发 history.listen(),调用 useReducer中的 dispatch,进而改变原本存储在 <Router>中的 location状态,重复以上第二步骤

Tips:如果本文有错误欢迎各位同学指出,谢谢啦😉

参考资料

React-router-dom | 原理解析

声明:本文内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎进行举报,并提供相关证据,工作人员会在5个工作日内联系你,一经查实,本站将立刻删除涉嫌侵权内容。