前言,首先为什么要做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的对象以及里面的childrenreturn <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.Providervalue={{history: this.props.history,location: this.state.location,match: Router.computeRootMatch(this.state.location.pathname),staticContext: this.props.staticContext}}><HistoryContext.Providerchildren={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源码解析
|