写在前面:为何要学习react-router底层源码? 为何要弄明白整个路由流程? 笔者我的感受学习react-router,有助于咱们学习单页面应用(spa)路由跳转原理,让咱们理解从history.push,到组件页面切换的全套流程,使咱们在面试的时候再也不为路由相关的问题发怵,废话不说,让咱们开启深刻react-router源码之旅吧。css
什么是单页面应用?html
我的理解,单页面应用是使用一个html下,一次性加载js, css等资源,全部页面都在一个容器页面下,页面切换实质是组件的切换。前端
react-router-dom
和react-router
和history
库三者什么关系history
能够理解为react-router
的核心,也是整个路由原理的核心,里面集成了popState,history.pushState
等底层路由实现的原理方法,接下来咱们会一一解释。react
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方法
api
react-router-dom
咱们很少说了,这里咱们重点看一下react-router
。浏览器
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>
}
复制代码
效果以下markdown
单页面应用路由实现原理是,切换url,监听url变化,从而渲染不一样的页面组件。react-router
主要的方式有history
模式和hash
模式。app
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()
方法。
window.location.hash
经过window.location.hash
属性获取和设置 hash
值。
onhashchange
window.addEventListener('hashchange',function(e){
/* 监听改变 */
})
复制代码
react-router
路由离不开history
库,history专一于记录路由history状态,以及path改变了,咱们应该作写什么, 在history模式下用popstate
监听路由变化,在hash模式下用hashchange
监听路由的变化。
接下来咱们看 Browser
模式下的createBrowserHistory
和 Hash
模式下的 createHashHistory
方法。
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
是监听路由变化底层方法。
const setState = (nextState) => {
/* 合并信息 */
Object.assign(history, nextState)
history.length = globalHistory.length
/* 通知每个listens 路由已经发生变化 */
transitionManager.notifyListeners(
history.location,
history.action
)
}
复制代码
代码很简单:统一每一个transitionManager
管理的listener
路由状态已经更新。
何时绑定litener
, 咱们在接下来的React-Router
代码中会介绍。
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
方法。
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
,触发组件更新。
最后咱们来看看当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
,重新加载组件。
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
库总体流程。
Router
做用是把 history location
等路由信息 传递下去
Router
/* Router 做用是把 history location 等路由信息 传递下去 */
class Router extends React.Component {
static computeRootMatch(pathname) {
return { path: '/', url: '/', params: {}, isExact: pathname === '/' };
}
constructor(props) {
super(props);
this.state = {
location: props.history.location
};
//记录pending位置
//若是存在任何<Redirect>,则在构造函数中进行更改
//在初始渲染时。若是有,它们将在
//在子组件身上激活,咱们可能会
//在安装<Router>以前获取一个新位置。
this._isMounted = false;
this._pendingLocation = null;
/* 此时的history,是history建立的history对象 */
if (!props.staticContext) {
/* 这里判断 componentDidMount 和 history.listen 执行顺序 而后把 location复制 ,防止组件从新渲染 */
this.unlisten = props.history.listen(location => {
/* 建立监听者 */
if (this._isMounted) {
this.setState({ location });
} else {
this._pendingLocation = location;
}
});
}
}
componentDidMount() {
this._isMounted = true;
if (this._pendingLocation) {
this.setState({ location: this._pendingLocation });
}
}
componentWillUnmount() {
/* 解除监听 */
if (this.unlisten) this.unlisten();
}
render() {
return (
/* 这里能够理解 react.createContext 建立一个 context上下文 ,保存router基本信息。children */
<RouterContext.Provider children={this.props.children || null} value={{ history: this.props.history, location: this.state.location, match: Router.computeRootMatch(this.state.location.pathname), staticContext: this.props.staticContext }} />
);
}
}
复制代码
总结:
初始化绑定listen, 路由变化,通知改变location
,改变组件。 react的history路由状态是保存在React.Content
上下文之间, 状态更新。
一个项目应该有一个根Router
, 来产生切换路由组件以前的更新做用。 若是存在多个Router
会形成,会形成切换路由,页面不更新的状况。
根据router更新流,来渲染当前组件。
/* switch组件 */
class Switch extends React.Component {
render() {
return (
<RouterContext.Consumer>
{/* 含有 history location 对象的 context */}
{context => {
invariant(context, 'You should not use <Switch> outside a <Router>');
const location = this.props.location || context.location;
let element, match;
//咱们使用React.Children.forEach而不是React.Children.toArray().find()
//这里是由于toArray向全部子元素添加了键,咱们不但愿
//为呈现相同的两个<Route>s触发卸载/从新装载
//组件位于不一样的URL。
//这里只需然第一个 含有 match === null 的组件
React.Children.forEach(this.props.children, child => {
if (match == null && React.isValidElement(child)) {
element = child;
// 子组件 也就是 获取 Route中的 path 或者 rediect 的 from
const path = child.props.path || child.props.from;
match = path
? matchPath(location.pathname, { ...child.props, path })
: context.match;
}
});
return match
? React.cloneElement(element, { location, computedMatch: match })
: null;
}}
</RouterContext.Consumer>
);
}
}
复制代码
找到与当前path,匹配的组件进行渲染。 经过pathname
和组件的path
进行匹配。找到符合path的router组件。
function matchPath(pathname, options = {}) {
if (typeof options === "string" || Array.isArray(options)) {
options = { path: options };
}
const { path, exact = false, strict = false, sensitive = false } = options;
const paths = [].concat(path);
return paths.reduce((matched, path) => {
if (!path && path !== "") return null;
if (matched) return matched;
const { regexp, keys } = compilePath(path, {
end: exact,
strict,
sensitive
});
const match = regexp.exec(pathname);
/* 匹配不成功,返回null */
if (!match) return null;
const [url, ...values] = match;
const isExact = pathname === url;
if (exact && !isExact) return null;
return {
path, // the path used to match
url: path === "/" && url === "" ? "/" : url, // the matched portion of the URL
isExact, // whether or not we matched exactly
params: keys.reduce((memo, key, index) => {
memo[key.name] = values[index];
return memo;
}, {})
};
}, null);
}
复制代码
匹配符合的路由。
/** * The public API for matching a single path and rendering. */
class Route extends React.Component {
render() {
return (
<RouterContext.Consumer>
{context => {
/* router / route 会给予警告警告 */
invariant(context, "You should not use <Route> outside a <Router>");
// computedMatch 为 通过 swich处理后的 path
const location = this.props.location || context.location;
const match = this.props.computedMatch
? this.props.computedMatch // <Switch> already computed the match for us
: this.props.path
? matchPath(location.pathname, this.props)
: context.match;
const props = { ...context, location, match };
let { children, component, render } = this.props;
if (Array.isArray(children) && children.length === 0) {
children = null;
}
return (
<RouterContext.Provider value={props}>
{props.match
? children
? typeof children === "function"
? __DEV__
? evalChildrenDev(children, props, this.props.path)
: children(props)
: children
: component
? React.createElement(component, props)
: render
? render(props)
: null
: typeof children === "function"
? __DEV__
? evalChildrenDev(children, props, this.props.path)
: children(props)
: null}
</RouterContext.Provider>
);
}}
</RouterContext.Consumer>
);
}
}
复制代码
匹配path,渲染组件。做为路由组件的容器,能够根据将实际的组件渲染出来。经过RouterContext.Consume
取出当前上一级的location,match
等信息。做为prop传递给页面组件。使得咱们能够在页面组件中的props中获取location ,match
等信息。
重定向组件, 若是来路由匹配上,会重定向对应的路由。
function Redirect({ computedMatch, to, push = false }) {
return (
<RouterContext.Consumer> {context => { const { history, staticContext } = context; /* method就是路由跳转方法。 */ const method = push ? history.push : history.replace; /* 找到符合match的location ,格式化location */ const location = createLocation( computedMatch ? typeof to === 'string' ? generatePath(to, computedMatch.params) : { ...to, pathname: generatePath(to.pathname, computedMatch.params) } : to ) /* 初始化的时候进行路由跳转,当初始化的时候,mounted执行push方法,当组件更新的时候,若是location不相等。一样会执行history方法重定向 */ return ( <Lifecycle onMount={() => { method(location); }} onUpdate={(self, prevProps) => { const prevLocation = createLocation(prevProps.to); if ( !locationsAreEqual(prevLocation, { ...location, key: prevLocation.key }) ) { method(location); } }} to={to} /> ); }} </RouterContext.Consumer>
);
}
复制代码
初始化的时候进行路由跳转,当初始化的时候,mounted
执行push
方法,当组件更新的时候,若是location
不相等。一样会执行history
方法重定向。
history
提供了核心api,如监听路由,更改路由的方法,已经保存路由状态state。
react-router
提供路由渲染组件,路由惟一性匹配组件,重定向组件等功能组件。
当地址栏改变url,组件的更新渲染都经历了什么?😊😊😊 拿history模式作参考。当url改变,首先触发histoy,调用事件监听popstate
事件, 触发回调函数handlePopState
,触发history下面的setstate
方法,产生新的location对象,而后通知Router组件更新location
并经过context上下文传递,
switch
经过传递的更新流,匹配出符合的Route组件渲染,最后有Route
组件取出context
内容,传递给渲染页面,渲染更新。
当咱们调用history.push
方法,切换路由,组件的更新渲染又都经历了什么呢?
咱们仍是拿history模式做为参考,当咱们调用history.push
方法,首先调用history的push
方法,经过history.pushState
来改变当前url
,接下来触发history下面的setState
方法,接下来的步骤就和上面如出一辙了,这里就不一一说了。
但愿读过此篇文章的朋友,可以明白react-router的整个流程,代码逻辑不是很难理解。整个流程我给你们分析了一遍,但愿同窗们能主动看一波源码,把整个流程搞明白。纸上得来终觉浅,绝知此事要躬行。
写在最后,谢谢你们鼓励与支持🌹🌹🌹,喜欢的能够给笔者点赞关注,公众号:前端Sharing