react-router核心原理
history
history 是一个用于提供类似浏览器 history 对象实现页面的无刷新跳转的库,该库是 react-router 的核心依赖。
history 库提供的功能:
- 三种 history:browserHistory、hashHistory、memoryHistory,提供相应的 create 函数并保持统一的 api
- 支持发布/订阅,当 URL 发生改变的时候,会发布订阅
- 提供跳转拦截、跳转确认和
basename
等功能
一个 history
对象所具有的属性:
1 | const history = { |
length
:历史记录堆栈的长度,实现也非常简单就是(globalHistory = window.history).length
action
:表示当前页面是通过什么行为进入的,取值为'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
组件完成。