react-router核心原理
history
history 是一个用于提供类似浏览器 history 对象实现页面的无刷新跳转的库,该库是 react-router 的核心依赖。
history 库提供的功能:
- 三种 history:browserHistory、hashHistory、memoryHistory,提供相应的 create 函数并保持统一的 api
- 支持发布/订阅,当 URL 发生改变的时候,会发布订阅
- 提供跳转拦截、跳转确认和
basename等功能
一个 history 对象所具有的属性:
1 | const history = { |
length:历史记录堆栈的长度,实现也非常简单就是(globalHistory = window.history).lengthaction:表示当前页面是通过什么行为进入的,取值为'POP'、'PUSH'、'REPLACE',默认值为'POP'location:和window.location类似,react-router 上下文中的location对象就是该对象createHref:根据location对象以及basename创建一个path字符串block:向页面添加一个阻塞,创建history的时候可以传递一个getUserConfirmation函数用于判断是否拦截跳转listen: 添加一个订阅,当 URL 发送改变会自动发布订阅
主要看 browserHistory 的实现
location
location 对象的格式:
1 | { |
location 具有的特点:
- 每个
location对象都具有一个唯一的key值 - 每个
location对象都具有state属性,该值是通过history.push所传递的值 - 如果 URL 存在 query 和 hash,
search属性必定以?开头,hash必定以#开头,否则举是空串
创建
该对象是通过 LocationUtil 模块下的 createLocation 所创建,当跳转到一个新地址时都会先创建一个新的 location 对象。
通过 history.push 可以传递几种格式的路径,都是该函数帮忙处理的:
- 可以传递对象格式的路径,
{pathname: xxx, hash: xxx, search: 'xxx'} - 可以传递完整的字符串路径
- 可以传递不完整的路径,会根据当前的路径进行补全
1 | function createLocation(path, state, key, currentLocation) { |
初始值
一个比较有意思的是 location 的初始值的实现:
初始的 location 是通过 window.location 和 window.history.state 创建,因为该函数可能是刚进入页面运行,也可能是通过刷新页面导致运行,但是刷新并不会清空 window.history.state 的值。
1 | // 初始的 location 值,通过 window.location 和 window.history.state 创建 |
key 值
比较好奇的是 location 对象的 key 属性的作用,create 函数内部维护了一个 allkeys 队列,和浏览器的历史记录堆栈类似,该队列也按照历史记录堆栈的顺序存放着相对一个的 location 的 key 值。
有什么用?不知道 😂😂😂,应该是为了后序新增的功能铺垫。在 revertPop 方法中写了 TODO:
1 | // TODO: We could probably make this more reliable by |
该函数没看出来对现有的功能有啥影响,感觉可以忽略该方法
transitionManager
因为 history 提供了跳转拦截,发布/订阅等功能,这也是在每次跳转时需要处理的一些事情。
history 通过 transitionManager 对象来实现这些功能,通过名字就能看出来,过渡管理,一看就是处理跟页面的过度有关功能。
该对象通过 createTransitionManager 创建,具有 setPrompt、confirmTransitionTo、appendListener、notifyListeners 几个方法,这些方法是 history 一些方法实现的核心。
每个 history 对象都对应一个 transitionManager 对象,在合适的时候只需调用对应的方法即可。
跳转拦截
先来回顾跳转拦截怎么使用,首先我们使用 history.block 设置一个阻塞,接着我们在 getUserConfirmation 中判断处理是否允许跳转,getUserConfirmation 接收两个参数:
- 参数 1:阻塞传递的消息,我们通过
block传递 - 参数 2:一个跳转的回调函数,向其传递
true允许跳转,传递false阻塞
setPrompt
history.block 本质上就是调用 setPrompt,该方法的的目的就是设置一个阻塞,是否跳转都是其他方法来处理,所以只需要用一个共享的变量来标识已经设置了拦截就可以了。
该方法的实现很简单,因为 transitionManager 是通过函数所创建,通过一个变量 prompt 利用闭包实现标识拦截与否。
1 | let prompt = null; |
confirmTransitionTo
通过函数名就能看出来,是否跳转由我来处理,而且该方法是一个通用的方法,当需要跳转时你调用我就行。
跳不跳转的事我来处理,你把 location 和实现跳转的方法作为 callback 传递给我,毕竟我不知道怎么跳转而且跳转的实现多种多样,而且跳转有时候需要 getUserConfirmation 来决定。
callback 的格式:给你传
true你就跳转,个人觉得跳转函数应该不接受参数,应该让此函数决定是否调用 callback 这样更加好。
1 | function confirmTransitionTo(location, action, getUserConfirmation, callback) { |
发布订阅
一个典型的发布订阅模式,要实现该功能必定有一个队列存放着监听函数,在 appendListener 时添加,在 notifyListeners 调用。
比较巧妙的是 appendListener 的实现,因为监听是可以取消的,所以必定要从队列中移除相对应的 listener,利用闭包非常的巧妙实现了这一点。
1 | let listeners = []; |
push
push 是整个 history 对象中最重要的方法,也是用的最多的方法。该方法的作用是改变 URL 地址顺便和可以给新的页面传递数据。
一旦进行了页面的跳转,history 对象就需要进行更新,action 和 location 肯定是需要改变的,这一点还是很好实现的。
而 URL 的改变直接使用 window.history.pushState 就可以很简单的实现。
由于可能会存在跳转拦截,所以必定需要调用 confirmTransitionTo 方法,然后将跳转的操作作为 callback 传递。
1 | function push(path, state) { |
setState 做的事很简单,就是更新 history 对象,并顺便分发一下订阅:
1 | // 用于更新 history ,以及分发 listener |
我们传递的 state 会被保存在两个地方,一个是 history.location.state,另一个是 window.history.state
replace 的实现很简单和 push 一样,不一样的仅仅是 aciton 的值为 'REPLACE',跳转调用的是 window.history.replaceState
block
由于 transitionManager 对象实现了 setPrompt 方法,所以 block 方法实现阻塞只需要调用该方法就可以,但是需要注册 popState 事件因为当使用浏览器前进后退时也需要进行跳转拦截的确认。
listen 的实现和 block 的实现类似,因为一旦设置了监听,跳转时需要分发订阅。
1 | let isBlocked = false; |
事件处理
当设置了 block / listen 时,当通过浏览器进行前进后退时,也需要跳转检测。
checkDOMListeners 方法实现了事件的添加和取消。
1 | // 添加 popState 事件处理,当使用 前进后退 时,也需要跳转检测,以及用于取消该事件处理 |
其他
basename
history 提供了 basename 功能,也就是说我们的项目可能不是部署在网站的根目录,在创建 history 的时候我们可以指定 basename,然后写项目时可以当作项目就是部署在根目录一样来写,history 帮我们处理的这个问题。
处理方式就是通过 createHref 的实现,该函数是创建一个 path 字符串,一般用于内部的实现。它的实现很简单:
1 | // 创建一个 path 字符串 |
getUserConfirmation
在我们使用 react-router 的过程中,发现没有传递 getUserConfirmation,有默认的处理方式,history 提供了该方法的默认实现:
1 | // 默认 gerUserConfirmation 实现 |
react-router
react-router 本身的实现是比较简单的,核心的功能都被 history 实现了。
只需要根据 path 的匹配与否来渲染对应的组件,以及提供上下文数据。主要就是实现一些用于路由的组件。
上下文
react-router 中定义了两个上下文,一个是 RouterContext,另一个是 HistoryContext。
react-router 中的上下文设计我不是很能理解,RouterContext.Provider 使用了两次,上下文中都是 history、location、match
但是第一个 RouterContext 中数据不是给我们使用的,而且其中的 match 和我们真正使用的 match 不一样,Route 组件又使用了一次该上下文,这次传递的 value 才是我们组件中真正使用到的数据。
HistoryContext 中的数据只有一个 history 对象,用于给 useHistory hook 提供 history,明明使用 RouterContext 就能够拿到需要的 history。
matchPath
该方法用于进行路径匹配,用于将当前的路径和配置的路径规则进行匹配,匹配成功返回 match 对象,否则返回 null。
核心是利用 path-to-regexp 库进行匹配,match 对象的格式:
1 | { |
每当需要进行路径匹配的时候都会调用该方法进行判断,我们给一个组件配置的路径规则和选项例如 exact,都会传递过来进行判断。
1 | function matchPath(pathname, options = {}) { |
Router
因为存在三种不同的路由组件,但是核心逻辑是相同的,Router 组件就是所有路由组件都使用的核心组件,只需要传递不同类型的 history 即可。
Router 组件做的事很简单,就是提供上下文数据,主要是给内部组件提供的,间接的给我们的组件提供。
其中进行了一些必要的处理,存在子组件在 Router 组件没有 mount 完毕就改变 URL 的情况。

Route
Route 组件的作用:
- 根据路径规则配置来配渲染我们的组件
- 为我们的组件提供上下文数据,以及将上下文中的数据作为
props传入
Route 组件可以传入 component、render、children,但是渲染的优先级是不同的,这里才是 Route 组件的核心。
优先级:children(函数)> children(node)> component(node) > render(函数)
当 children 为函数时,即使该路由没有匹配也会渲染。
当我们直接使用 Route 组件时,每个 Route 组件是一定会被渲染的,只不过会根据我们递的路径规则进行 mathPath 进行判断是否把我们的组件渲染出来。
而当我们使用了 Switch 组件时,只有匹配到的第一个才会被渲染,为了提高优先级 Switch 会传递 computedMatch 属性,其实就是一个 match 对象,只不过名称不同而已,我们也可以传递不过没必要。

Switch
Switch 组件用于渲染第一个匹配的 Route 组件 / Redirect 组件。
其实没必要一定是 Route / Redirect,只要一个组件传递了 path / from 属性,Switch 都会渲染。

Lifecycle
正如名字那样,这个组件不是用来渲染的,是用来在生命周期处理各种事情的,是一个工具组件。
1 | class Lifecycle extends React.Component { |
Redirect
Redirect 组件用于实现跳转,但是不能在 render 的时候就跳转,这就相当于在 render 时触发 rerender,不合理。所以需要在 cdm 中进行,利用写好的 Lifecycle 组件只需要传递回调就可以。
而且单独使用 Redirect 是没办法使用 from 属性进行匹配的,只有使用 Switch 时才可以使用,因为 from 的匹配在 Switch 组件完成。









