写在前面:为什么要学习react-router底层源码? 为什么要弄明白整个路由流程? 笔者个人感觉学习react-router,有助于我们学习单页面应用(spa)路由跳转原理,让我们理解从history.push,到组件页面切换的全套流程,使我们在面试的时候不再为路由相关的问题发怵,废话不说,让我们开启深入react-router源码之旅吧。
一 正确理解react-router
1 理解单页面应用
什么是单页面应用?
个人理解,单页面应用是使用一个html下,一次性加载js, css等资源,所有页面都在一个容器页面下,页面切换实质是组件的切换。
2 react-router初探,揭露路由原理面纱
①react-router-dom
和react-router
和history
库三者什么关系
history
可以理解为react-router
的核心,也是整个路由原理的核心,里面集成了popState,history.pushState
等底层路由实现的原理方法,接下来我们会一一解释。
react-router
可以理解为是react-router-dom
的核心,里面封装了Router,Route,Switch
等核心组件,实现了从路由的改变到组件的更新的核心功能,在我们的项目中只要一次性引入react-router-dom
就可以了。
react-router-dom
,在react-router
的核心基础上,添加了用于跳转的Link
组件,和histoy模式下的BrowserRouter
和hash模式下的HashRouter
组件等。所谓BrowserRouter和HashRouter,也只不过用了history库中createBrowserHistory和createHashHistory方法
react-router-dom
我们不多说了,这里我们重点看一下react-router
。
②来个小demo尝尝鲜?
import {
BrowserRouter as Router, Switch, Route, Redirect,Link } from 'react-router-dom'
import Detail from '../src/page/detail'
import List from '../src/page/list'
import Index from '../src/page/home/index'
const menusList = [
{
name: '首页',
path: '/index'
},
{
name: '列表',
path: '/list'
},
{
name: '详情',
path: '/detail'
},
]
const index = () => {
return <div >
<div >
<Router >
<div>{
/* link 路由跳转 */
menusList.map(router=><Link key={
router.path} to={
router.path } >
<span className="routerLink" >{
router.name}</span>
</Link>)
}</div>
<Switch>
<Route path={
'/index'} component={
Index} ></Route>
<Route path={
'/list'} component={
List} ></Route>
<Route path={
'/detail'} component={
Detail} ></Route>
{
/* 路由不匹配,重定向到/index */}
<Redirect from='/*' to='/index' />
</Switch>
</Router>
</div>
</div>
}
效果如下
二 单页面实现核心原理
单页面应用路由实现原理是,切换url,监听url变化,从而渲染不同的页面组件。
主要的方式有history
模式和hash
模式。
1 history模式原理
①改变路由
history.pushState
history.pushState(state,title,path)
1 state
:一个与指定网址相关的状态对象, popstate 事件触发时,该对象会传入回调函数。如果不需要可填 null。
2 title
:新页面的标题,但是所有浏览器目前都忽略这个值,可填 null。
3 path
:新的网址,必须与当前页面处在同一个域。浏览器的地址栏将显示这个地址。
history.replaceState
history.replaceState(state,title,path)
参数和pushState
一样,这个方法会修改当前的 history
对象记录, history.length
的长度不会改变。
②监听路由
popstate事件
window.addEventListener('popstate',function(e){
/* 监听改变 */
})
同一个文档的 history
对象出现变化时,就会触发 popstate
事件history.pushState
可以使浏览器地址改变,但是无需刷新页面。注意⚠️的是:用 history.pushState()
或者 history.replaceState()
不会触发 popstate
事件。 popstate
事件只会在浏览器某些行为下触发, 比如点击后退、前进按钮或者调用 history.back()、history.forward()、history.go()
方法。
2 hash模式原理
①改变路由
window.location.hash
通过window.location.hash
属性获取和设置 hash
值。
②监听路由
onhashchange
window.addEventListener('hashchange',function(e){
/* 监听改变 */
})
三 理解history库
react-router
路由离不开history
库,history专注于记录路由history状态,以及path改变了,我们应该做写什么,
在history模式下用popstate
监听路由变化,在hash模式下用hashchange
监听路由的变化。
接下来我们看 Browser
模式下的createBrowserHistory
和 Hash
模式下的 createHashHistory
方法。
1 createBrowserHistory
Browser模式下路由的运行 ,一切都从createBrowserHistory
开始。这里我们参考的history-4.7.2版本,最新版本中api可能有些出入,但是原理都是一样的,在解析history过程中,我们重点关注setState ,push ,handlePopState,listen
方法
const PopStateEvent = 'popstate'
const HashChangeEvent = 'hashchange'
/* 这里简化了createBrowserHistory,列出了几个核心api及其作用 */
function createBrowserHistory(){
/* 全局history */
const globalHistory = window.history
/* 处理路由转换,记录了listens信息。 */
const transitionManager = createTransitionManager()
/* 改变location对象,通知组件更新 */
const setState = () => {
/* ... */ }
/* 处理当path改变后,处理popstate变化的回调函数 */
const handlePopState = () => {
/* ... */ }
/* history.push方法,改变路由,通过全局对象history.pushState改变url, 通知router触发更新,替换组件 */
const push=() => {
/*...*/ }
/* 底层应用事件监听器,监听popstate事件 */
const listen=()=>{
/*...*/ }
return {
push,
listen,
/* .... */
}
}
下面逐一分析各个api,和他们之前的相互作用
const PopStateEvent = 'popstate'
const HashChangeEvent = 'hashchange'
popstate
和hashchange
是监听路由变化底层方法。
①setState
const setState = (nextState) => {
/* 合并信息 */
Object.assign(history, nextState)
history.length = globalHistory.length
/* 通知每一个listens 路由已经发生变化 */
transitionManager.notifyListeners(
history.location,
history.action
)
}
代码很简单:统一每个transitionManager
管理的listener
路由状态已经更新。
什么时候绑定litener
, 我们在接下来的React-Router
代码中会介绍。
②listen
const listen = (listener) => {
/* 添加listen */
const unlisten = transitionManager.appendListener(listener)
checkDOMListeners(1)
return () => {
checkDOMListeners(-1)
unlisten()
}
}
checkDOMListeners
const checkDOMListeners = (delta) => {
listenerCount += delta
if (listenerCount === 1) {
addEventListener(window, PopStateEvent, handlePopState)
if (needsHashChangeListener)
addEventListener(window, HashChangeEvent, handleHashChange)
} else if (listenerCount === 0) {
removeEventListener(window, PopStateEvent, handlePopState)
if (needsHashChangeListener)
removeEventListener(window, HashChangeEvent, handleHashChange)
}
}
listen本质通过checkDOMListeners
的参数 1 或 -1 来绑定/解绑 popstate
事件,当路由发生改变的时候,调用处理函数handlePopState**
。
接下来我们看看push
方法。
③push
const push = (path, state) => {
const action = 'PUSH'
/* 1 创建location对象 */
const location = createLocation(path, state, createKey(), history.location)
/* 确定是否能进行路由转换,还在确认的时候又开始了另一个转变 ,可能会造成异常 */
transitionManager.confirmTransitionTo(location, action, getUserConfirmation, (ok) => {
if (!ok)
return
const href = createHref(location)
const {
key, state } = location
if (canUseHistory) {
/* 改变 url */
globalHistory.pushState({
key, state }, null, href)
if (forceRefresh) {
window.location.href = href
} else {
/* 改变 react-router location对象, 创建更新环境 */
setState({
action, location })
}
} else {
window.location.href = href
}
})
}
push ( history.push )
流程大致是 首先生成一个最新的location
对象,然后通过window.history.pushState
方法改变浏览器当前路由(即当前的path),最后通过setState
方法通知React-Router
更新,并传递当前的location对象,由于这次url变化的,是history.pushState
产生的,并不会触发popState
方法,所以需要手动setState
,触发组件更新。
④handlePopState
最后我们来看看当popState
监听的函数,当path
改变的时候会发生什么,
/* 我们简化一下handlePopState */
const handlePopState = (event)=>{
/* 获取当前location对象 */
const location = getDOMLocation(event.state)
const action = 'POP'
transitionManager.confirmTransitionTo(location, action, getUserConfirmation, (ok) => {
if (ok) {
setState({
action, location })
} else {
revertPop(location)
}
})
}
handlePopState
代码很简单 ,判断一下action类型为pop
,然后 setState
,从新加载组件。
2 createHashHistory
hash 模式和 history API类似,我们重点讲一下 hash模式下,怎么监听路由,和push , replace
方法是怎么改变改变路径的。
监听哈希路由变化
const HashChangeEvent = 'hashchange'
const checkDOMListeners = (delta) => {
listenerCount += delta
if (listenerCount === 1) {
addEventListener(window, HashChangeEvent, handleHashChange)
} else if (listenerCount === 0) {
removeEventListener(window, HashChangeEvent, handleHashChange)
}
}
和之前所说的一样,就是用hashchange
来监听hash路由的变化。
改变哈希路由
/* 对应 push 方法 */
const pushHashPath = (path) =>
window.location.hash = path
/* 对应replace方法 */
const replaceHashPath = (path) => {
const hashIndex = window.location.href.indexOf('#')
window.location.replace(
window.location.href.slice(0, hashIndex >= 0 ? hashIndex : 0) + '#' + path
)
}
在hash
模式下 ,history.push
底层是调用了window.location.href
来改变路由。history.replace
底层是掉用window.location.replace
改变路由。
总结
我们用一幅图来描述了一下history
库整体流程。