前言,首先为什么要做react-router源码解析呢,因为之前我们有一个需求,左侧导航栏检测到路由变化的时候展示不同的样式。 由于左侧导航栏没有包裹到路由Router组件中,所以左侧导航栏无法准确监测路由发生了变化,当时我想了想可以这样解决
一、我想象中的解决方案
方案一 onhashchange
onhashchange就可以检测了,但是项目中的路由是history。onhashchange不能在history模式下用,所以这一条算废了。
方案二、onpopstate
但是mdn告诉说事情不是这么简单。mdn明确说go与back可以检测到但是pushState与replaceState检测不到,由于项目中大量使用push这些方法。所以这一条也废弃了。但是为什么项目中history.push调用的时候react-router能感应到并做出路由切换,难道是黑科技吗,想到这里我就想对react-router源码进行解析为什么react-router可以感应到history.push方法。
请注意react-router-dom可以结构出来useHistory方法,执行该方法可得到已经封装好的history对象,该对象有push方法,实际上执行的是pushState方法,replace其实也就是replaceState方法,这里说的push方法大家可以理解成执行pushState方法。
方案三、redux
当使用history.push这些方法的时候往redux传入我们要跳转到哪个路由,然后谁要监测这个路由谁就订阅这个数据源。这是个好办法,但是由于项目中使用history.push的地方有点多,也就是入口多,需要在跳转的时候都要派发一次数据虽然可以解决监测路由的问题,但是写了大量重复代码。我们在方案4中采用了redux的思想。
方案四、路由守卫
我记得vue的路由中有路由守卫概念,当进入到路由之前的话,左侧导航栏变化不同的样式。我们用react-router实现路由守卫不就可以了吗,当组件检测到路由变化的时候这时候就dispatch派发路由数据。 下面就是我们当初写的一个很low的路由守卫
import { useEffect } from 'react';
import {BrowserRouter as Router, Switch, Route, useLocation} from 'react-router-dom';
import Home from './home';
const RouteGuard = (props)=> {
const {pathname} = useLocation();
useEffect(()=> {
console.log('路由变化,',pathname);
//可以dispatch派发pathname到redux中,然后左侧导航栏订阅这个数据就可以切换主题了,
//这里也可以做路由权限的鉴定哈。如果没有权限写一些重定向的逻辑跳转到403页面。
//这个return函数就相当于路由销毁的时候我们要干一些什么事情
return ()=> {
}
});
return props.children;
};
const App =()=> {
return (
<Router>
<RouteGuard>
<Switch>
<Route path='/' exact>
<Home/>
</Route>
</Switch>
</RouteGuard>
</Router>
);
}
export default App;
二、react-router是怎么监听路由的改变的呢。
刚刚聊到react-router能够监测history.push方法执行并返回正确的路由。这里面react-router肯定监听了路由的改变但肯定不是用onpopstate监听的,因为onpopstate是监听不到push与replace改变的。为了解决这个谜团,上手解析源码。
注意📢
react-router源码没有多少行,如果解析源码大家看不懂,那就是我没讲明白。是我的锅。
三,源码解析
写路由一开始都是这样的
import {BrowserRouter as Router, Switch, Route} from 'react-router-dom';
import Home from './home';
const App =()=> {
return (
<Router>
<Switch>
<Route path='/' exact>
<Home/>
</Route>
</Switch>
</Router>
);
}
export default App;
我们就一段一段解析先解析Router然后解析Switch最后解析Route
1、解析Router组件
解析Router组件也就是解析BrowserRouter,因为导入BrowserRouter的时候重新命名为Router了。所以我们看一下BrowserRouter的源码。再次强调没有多少行,大家放下包袱不用担心源码看不懂。
//我在代码中一一注释他们分别干了什么
import React from "react";
import { Router } from "react-router";
import { createBrowserHistory as createHistory } from "history";
class BrowserRouter extends React.Component {
//createHistory(this.props),是调用了history的createBrowserHistory方法这个就是刚刚我们聊的
//为什么执行push相当于执行原生的pushstate是因为createHistory给我们封装好了
history = createHistory(this.props);
render() {
//这个就是调用react-router中的Router组件来传入了封装好的history的对象以及里面的children
return <Router history={this.history} children={this.props.children} />;
}
}
export default BrowserRouter;
1.1 createHistory 干了什么
这是截取的一部分核心代码,其实刚一看这些代码有点懵,毕竟代码有点多,其实我们只需要关注push这个方法就行其他的就可以猜出来,我们先看push方法,我在push方法写有注释。
function createBrowserHistory(props = {}) {
const globalHistory = window.history;
const canUseHistory = supportsHistory();
const needsHashChangeListener = !supportsPopStateOnHashChange();
const {
forceRefresh = false,
getUserConfirmation = getConfirmation,
keyLength = 6
} = props;
function checkDOMListeners(delta) {
listenerCount += delta;
if (listenerCount === 1 && delta === 1) {
window.addEventListener(PopStateEvent, handlePopState);
if (needsHashChangeListener)
window.addEventListener(HashChangeEvent, handleHashChange);
} else if (listenerCount === 0) {
window.removeEventListener(PopStateEvent, handlePopState);
if (needsHashChangeListener)
window.removeEventListener(HashChangeEvent, handleHashChange);
}
}
function push(path, state) {
const action = 'PUSH';
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) {
//看这个,这个globalHistory其实就是window.history所以我们实际上走的也是pushState这个方法,
globalHistory.pushState({ key, state }, null, href);
if (forceRefresh) {
window.location.href = href;
} else {
const prevIndex = allKeys.indexOf(history.location.key);
const nextKeys = allKeys.slice(0, prevIndex + 1);
nextKeys.push(location.key);
allKeys = nextKeys;
//这里就是通知组件重新渲染先不要管。我们知道react-router帮我们封装了这些方法。路由发生改变的时候
//就会调用setState通知组件重新渲染,注意这个setState与react的setState不一样,这个setState
//是一个封装好的函数,也就是下方的setState看一下在setState些的注释。
setState({ action, location });
}
} else {
window.location.href = href;
}
}
);
}
function setState(nextState) {
Object.assign(history, nextState);
history.length = globalHistory.length;
//transitionManager.notifyListeners调用了listeners.forEach(listener => listener(...args));
//...args 就是location信息
// listeners里面的数据就是在router组件中的componentDidMount调用
//this.props.history.listen(location =>
// this.setState({ location });
//});传递进去的最终就会执行this.setState({ location });使组件重新渲染
//
transitionManager.notifyListeners(history.location, history.action);
}
//这些其实简单来说就是调用原生的方法 checkDOMListeners这个方法中
//window.addEventListener(PopStateEvent, handlePopState);会监听go函数变化从而触发
//setState重新渲染
function go(n) {
globalHistory.go(n);
}
//这个就是初始化的时候监听的路由改变的事件
function checkDOMListeners(delta) {
listenerCount += delta;
if (listenerCount === 1 && delta === 1) {
window.addEventListener(PopStateEvent, handlePopState);
}
}
function goBack() {
go(-1);
}
function goForward() {
go(1);
}
const history = {
length: globalHistory.length,
action: 'POP',
location: initialLocation,
createHref,
push,
replace,
go,
goBack,
goForward,
block,
listen
};
return history;
}
export default createBrowserHistory;
总结以上代码createHistory帮我们封装了history对象,在push方法中触发了setState使组件重新渲染而go方法则是通过监听popstate事件触发setState方法。
1.2 Router组件干了什么
先贴精简过后的源码
import React from "react";
//HistoryContext与RouterContext都是Context.Provider组件
import HistoryContext from "./HistoryContext.js";
import RouterContext from "./RouterContext.js";
class Router extends React.Component {
static computeRootMatch(pathname) {
return { path: "/", url: "/", params: {}, isExact: pathname === "/" };
}
constructor(props) {
super(props);
this.state = {
location: props.history.location
};
}
componentDidMount() {
//📢setState方法我讲的是上一节说的封装好的setState方法而不是react解构出来的setSate方法,react解构出来
//的方法我会说成this.setState大家注意不要记乱了
//我们讲到过路由变化最终会触发setState方法,而setState方法最终会调用
//listeners.forEach(listener => listener(...args));这个方法,而listeners里面的数据已经通过
//this.props.history.listen(location =>
// this.setState({ location });
//});挂载了。所以会执行 this.setState({ location });重新渲染组件,我在之前的掘金文章中提到过执行
//setState对props.children无用,但是这是Context组件,会打上forceUpdate的tag标签从而重新渲染。
//剩下的事情就交给Switch组件了
this.props.history.listen(location =>
this.setState({ location });
});
}
componentWillUnmount() {
if (this.unlisten) {
this.unlisten();
}
}
render() {
return (
<RouterContext.Provider
value={{
history: this.props.history,
location: this.state.location,
match: Router.computeRootMatch(this.state.location.pathname),
staticContext: this.props.staticContext
}}
>
<HistoryContext.Provider
children={this.props.children || null}
value={this.props.history}
/>
</RouterContext.Provider>
);
}
}
export default Router;
这两个setState方法有点乱,大家去看一下react-router源码并debugger以下就能更好的区分了。我这里尽力去讲。
2、解析Switch组件
我们聊完了Router组件,接下来就是Switch组件了,我们看一下
import React from "react";
import RouterContext from "./RouterContext.js";
import matchPath from "./matchPath.js";
class Switch extends React.Component {
render() {
return (
<RouterContext.Consumer>
{context => {
const location = this.props.location || context.location;
let element, match;
React.Children.forEach(this.props.children, child => {
//这个就是switch组件的里面处理逻辑他是匹配switch中的第一个符合条件的route组件
if (match == null && React.isValidElement(child)) {
element = child;
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>
);
}
}
export default Switch;
其实Switch组件源码就是可简单,就是拿到子组件的path或者form属性与context传递的location属性进行比较,如果符合就返回该route组件。如果我们去掉switch组件的话直接写route组件,这个页面也会正常渲染的,但是容易出现bug如果匹配到两个相同的路由他就会讲两个相同的路由显示到页面上,而不是只显示匹配到的第一个路由.
3、解析route组件
route解析完成之后我们的react-router源码解析工作也快要结束了,话不多少上源码
import React from "react";
import RouterContext from "./RouterContext.js";
import matchPath from "./matchPath.js";
function isEmptyChildren(children) {
return React.Children.count(children) === 0;
}
class Route extends React.Component {
render() {
return (
<RouterContext.Consumer>
{context => {
const location = this.props.location || context.location;
//通过传递过来的location与path来比较如果匹配成功就显示,如果匹配不成功就不显示,其实很简单的
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 } = this.props;
if (Array.isArray(children) && isEmptyChildren(children)) {
children = null;
}
return (
<RouterContext.Provider value={props}>
{props.match? children: null}
</RouterContext.Provider>
);
}}
</RouterContext.Consumer>
);
}
}
export default Route;
总结一下 history.push 会触发 自己封装的 setState({ action, location })
这个方法,这个方法会调用 transitionManager.notifyListeners(history.location, history.action)
;去执行listeners.forEach(listener => listener(...args));
而listeners在我们挂载组件的时候已经向listeners数组中传递了
location => { this.setState({ location }) };
所以经过这一系列流程我们history.push会调用到this.setState方法,从而利用context模式重新渲染。
好了,我们的react-router源码解析也到此结束了,hash模式没有讲,因为其实和history没什么区别就留给大家debuger吧,哈哈,我们下期再见 下期计划
1、写一个keep-alive缓存组件
2、react源码解析