react-router源码完全解读
注:react-router版本为v5.2.0
预备知识
1.前端基础:history、location。
2.react: refs转发、context、useContext(react Hooks)。
3.依赖库:
history(^4.9.0)
path-to-regexp(^1.7.0):主要用到pathToRegexp.compile(path)、pathToRegexp(path, keys, options)两个方法
history库(^4.9.0)
history库是react-router依赖的核心库,它将应用的history做了统一的抽象,包含一系列统一的属性和方法,支持浏览器的BrowserHistory、HashHistory以及服务端的MemoryHistory。
createBrowserHistory的属性和方法
1 | length: globalHistory.length, |
createHashHistory的属性和方法
1 | length: globalHistory.length, |
createMemoryHistory的属性和方法
1 | length: entries.length, |
接下来我们讲解一下这三种history的具体实现。
createTransitionManager
createTransitionManager可以创建一个TransitionManager来帮助history管理各种行为,它被三种history都使用了,我们先来介绍它。
这是createTransitionManager的主要功能代码,很容易理解,就是实现了一个发布订阅模式。
1 | let listeners = []; |
setPrompt()是显示可提示用户进行输入的对话框的意思,这个功能主要是为了一些典型场景,比如:用户点击手机的返回键,让用户确认是否返回上一个url。
1 | let prompt = null; |
confirmTransitionTo在history的行为方法中(push、pop、replace)都会被调用,它的作用是拦截每个行为,让用户或开发者确认能否执行这个行为。
1 | function confirmTransitionTo( |
例如,push方法中confirmTransitionTo是这样使用的,在第四个参数callback中根据返回值是否为true,判断是否真正执行push行为。
1 | function push(path, state) { |
history.listen
history.listen在浏览器中主要是利用DOM方法进行事件监听的绑定和取消。
browserHistory中的实现
browserHistory使用的是popstate和hashchange事件。
同时会将监听触发的回调函数添加到前面介绍的transitionManager中,这样监听触发时只需要通过执行transitionManager.notifyListeners()发送通知,执行这些回调函数就可以了。
1 | const PopStateEvent = 'popstate'; |
在很多浏览器中hash change也会触发popstate事件,所以hashchange事件在browserHistory中也是需要监听的。
1 | const needsHashChangeListener = !supportsPopStateOnHashChange(); |
hashHistory中的实现
hashHistory中只需要监听hashchange事件就可以了
1 | const HashChangeEvent = 'hashchange'; |
memoryHistory中的实现
memoryHistory不需要监听事件,它只需要将监听触发的回调函数添加到transitionManager中就可以了。因为它是服务端主动控制的路由,不需要监听被动的路由改变,进而执行一些状态更新。
1 | function listen(listener) { |
browserHistory中handlePopState的实现
hashHistory和memoryHistory是没有popState事件的,所以不需要实现它们。
handlePopState主要会执行handlePop方法,handlePop主要会执行setState方法,setState方法主要是合并了history状态,通过transitionManager.notifyListeners通知了添加的listener函数执行。
getDOMLocation生成的就是我们经常见到的location参数。
1 | { |
1 | function handlePopState(event) { |
confirmTransitionTo的回调函数范围为false的时候,说明禁止进行这次路由操作。它调用revertPop方法实现,通过计算此次路由操作的delta,调用go(delta)方法将路由恢复到原来的状态,go方法就是原生的history.go方法。
1 | function revertPop(fromLocation) { |
handleHashChange
browserHistory中的实现
它调用的其实主要也是handlePop方法
1 | function getHistoryState() { |
hashHistory中的实现
path !== encodedPath这个判断是为了让我们总是有标准的hash路径,后面的操作判断主要是判断一下前后的location是否相同、是否是ignorePath,如果都不是,则会执行handlePop方法。
1 | function handleHashChange() { |
handlePop和前面browserHistory介绍的是类似的,有区别的地方是revertPop使用的allPaths作为history的索引,browserHistory使用的allKeys。
1 | function handlePop(location) { |
allPaths是完整的路径
1 | export function createPath(location) { |
allKeys是随机的key
1 | function createKey() { |
history.push
browserHistory中的实现
history.push方法很简单,主要调用了history.pushState方法。由于allKeys维护了所有history state中的key,所以在push方法需要做相应的处理。
1 | const globalHistory = window.history; |
hashHistory中的实现
history.push方法很简单,主要调用了window.location.hash方法。由于allPaths维护了所有的path,所以在push方法需要做相应的处理。
1 | function push(path, state) { |
memoryHistory中的实现
由于是在内存中维护history的状态,所以主要是history.entries(所有history location列表)的维护。
1 | function push(path, state) { |
history.replace()
browerHistory中的实现
history.replace方法和push是类似的,主要调用了history.replaceState方法。
1 | function replace(path, state) { |
hashHistory中的实现
replace在hashHistory中的实现也很简单,主要调用了window.location.replace方法。
1 | function replace(path, state) { |
memoryHistory中的实现
1 | function replace(path, state) { |
history.go()、history.goBack()、history.goForward()
browserHistory、hashHistory中的实现
1 | function go(n) { |
memoryHistory中的实现
计算nextIndex(一般为history.index + n),执行POP action即可。
1 | function clamp(n, lowerBound, upperBound) { |
history.block()
browserHistory、hashHistory中的实现
block提供了setPrompt的调用接口,因为我们前面介绍过,push、pop、replace action都是在transitionManager.confirmTransitionTo的回调函数中执行的,只有回调函数返回true,才能真正执行这些action。而前面我们看到回调函数的返回结果其实是由用户传递的prompt方法决定的,这样就可以让用户根据自己的逻辑决定是否阻塞路由跳转了。
1 | let isBlocked = false; |
memoryHistory中的实现
memoryHistory不需要做DOM事件监听的相关处理。
1 | function block(prompt = false) { |
react-router
我们之所以大篇幅介绍history库,是因为history库才是路由管理的底层逻辑,react-router其实只是使用react框架封装了history库的处理(主要使用context跨组件传递history的状态和方法)。介绍到这,你是不是已经能够大致勾勒出诸如<BrowserRouter>
、<Route>
、<Switch>
、<Link>
、withRouter()
等的简单实现了呢?介绍来让我们看看react-router中具体是怎么实现的。
createNamedContext()
该方法可以创建有displayName的context。
1 | // TODO: Replace with React.createContext once we can assume React 16+ |
generatePath()
生成路径,主要调用的是pathToRegexp.compile()方法,generatePath可以根据路径path和参数params生成完整的路径。比如('/a/:id', { id: 1 }) -> '/a/1'
。
1 | import pathToRegexp from "path-to-regexp"; |
matchPath()
该方法传入pathname,以及解析pathname的配置,可以得到从pathname中匹配的结果。这是我们使用react-router经常见到的数据,没错,它就是通过matchPath方法解析的。
1 | return { |
1 | import pathToRegexp from "path-to-regexp"; |
historyContext
创建historyContext。
1 | import createNamedContext from "./createNameContext"; |
routerContext
创建routerContext。这里源码的写法有冗余了。
1 | // TODO: Replace with React.createContext once we can assume React 16+ |
Lifecycle
创建一个react组件,它是一个空组件,主要是为了在组件生命周期的各个阶段能够调用用户通过props传入的回调函数。
1 | import React from "react"; |
Router
<Router>
是我们很常用的组件,有了前面的知识铺垫,它的实现就非常简单了。
组件内部有一个location的state,如果不是静态路由,通过history.listen方法监听history的变化。这里的history就是我们前面介绍的history库生成的history,它可以采用browserHistory、hashHistory、memoryHistory,history库对这三种history做了一致的接口封装。history如果发生改变,就是调用this.setState({ location })
,组件重新渲染,RouterContext.Provider、HistoryContext.Provider的值更新,它们下面的跨级组件也能感知到,从而获得最新的参数和方法。
1 | import React from "react"; |
Route
使用RouterContext.Consumer可以感知到上层RouterContext.Provider值的变动,从而自动计算match,根据match的结果渲染匹配的业务组件(使用props传入children, component, render方法之一)。
如果有computedMatch属性说明在<Switch>
组件中已经计算了match,可以直接使用。Switch组件我们后面会介绍。
1 | import React from "react"; |
Redirect
重定向组件根据传入的push属性可以决定使用history.push还是history.replace进行重定向,根据传入computedMatch, to可以计算出重定向的location。如果在静态组件中,会直接执行重定向。如果不是,采用使用空组件Lifecycle,在组件挂载阶段重定向,在onUpdate中判断重定向是否完成。
1 | import React from "react"; |
Switch
被Switch组件包裹的组件只会渲染其中第一个路由匹配成功的组件。
主要通过React.Children.forEach(this.props.children, child => {}),遍历出第一个匹配的路由及组件,并通过React.cloneElement返回这个组件。
1 | import React from "react"; |
StaticRouter
静态路由组件自己实现了一个简单的history,没有监听history变化的概念,也不需要go、goBack、goForward、listen、block方法。
1 | import React from "react"; |
MemoryRouter
MemoryRouter的history指定使用了createMemoryHistory,内部逻辑就是Router的逻辑。
1 | import React from "react"; |
Prompt
Prompt组件当Router不是staticRouter且when属性为true时才生效。
调用的是前面介绍的history.block()方法。
1 | import React from "react"; |
withRouter
由于从RouterContext.Consumer的context中可以很方便取到路由参数,所以withRouter就很容易实现了。只需要使用高阶组件的形式,接收被包裹组件作为参数,将context作为参数传入被包裹组件组件,再返回这个组件即可。
component还暴露了wrappedComponentRef属性,可以转发ref。
1 | import React from "react"; |
hooks
react-router还使用了useContext hook使用react hook的方式来提供一些路由参数和history。
1 | import React from "react"; |
react-router-dom
react-router中还包括react-router-dom库的实现,来提供dom相关的路由操作。
我们在react工程中一般使用的就是react-router-dom库,它的底层是前面介绍的react-router。
BrowserRouter
我们在项目中使用HTML5 history控制路由,可以直接使用react-router-dom中的BrowserRouter。
1 | import React from "react"; |
HashRouter
我们在项目中使用window.location.hash控制路由,可以直接使用react-router-dom中的HashRouter。
1 | import React from "react"; |
Link
<Link>
组件是react-router中常见的路由跳转组件。它使用的是html的a标签,为其绑定了点击事件。用户点击时,既可以执行用户自定义的onClick回调函数,也会执行navigate -> method(location),method可以根据用户传入的replace参数决定是使用history.replace还是history.push,同时点击事件也会阻止事件冒泡以免产生副作用。
Link还暴露了forwardedRef属性,可以转发ref。
1 | import React from "react"; |
NavLink
NavLink是基于Link的,它主要功能是可以自定义设置一些activeStyle、className,从而改变Link的样式。
1 | import React from "react"; |
react-router-config
react-router-config是为了方便我们使用类似下面的配置来编写react-router
1 | const routes = [ |
它只有两个api,matchRoutes和renderRoutes。
matchRoutes
1 | import { matchPath, Router } from "react-router"; |
renderRoutes
renderRoutes在组件中使用,可以根据前面的路由配置渲染相应的组件。
1 | import React from "react"; |
使用示例如下:
1 | import { renderRoutes } from "react-router-config"; |
react-router-native
react-router里最后一个包是react-router-native,因为没有做过相关业务,就没有研究了。