原因前端
目前负责的项目中有一个微信网页,用的是react技术栈。在该项目中增长了一个微信分享功能后,线上ios出现了问题,经排查,定位到了react的路由系统。 react
此次线上bug,让我决定,先拿react-router-dom开刀,看看它内部到底干了点啥,以解心头之恨。ios
前端目前用到的就是react-router-dom这个库,它提供了两个高级路由器,分别是BrowserRouter和HashRouter,它两的区别就是一个用的history API ,一个是使用URL的hash部分,接下来我以BrowserRouter为例,作一个解读。web
简易过程图数组
解读(只摘取核心代码进行展现)浏览器
首先看看整个react-router-dom提供了点啥?微信
export {
MemoryRouter, Prompt, Redirect, Route, Router, StaticRouter, Switch, generatePath, matchPath, withRouter, useHistory, useLocation, useParams, useRouteMatch } from "react-router"; export { default as BrowserRouter } from "./BrowserRouter.js"; export { default as HashRouter } from "./HashRouter.js"; export { default as Link } from "./Link.js"; export { default as NavLink } from "./NavLink.js"; 复制代码
除了下面它本身实现的四个组件外,其他的都是将react-router提供的组件作了一个引入再导出,那看来底层核心的东西仍是在react-router上。网络
import { BrowserRouter, Route, Switch, Link } from "react-router-dom"
function App() { return ( <BrowserRouter> <div>主菜单</div> <Link to="/home">home</Link> <br /> <Link to="/search">search</Link> <hr /> <Switch> <Route path="/home" component={Home} /> <Route path="/search" component={Search} /> </Switch> </BrowserRouter> ) } ReactDOM.render(<App />, document.getElementById('root')); 复制代码
须要经过路由跳转来实现UI变化的组件,要用BrowserRouter做为一个根组件来包裹起来,Route用来盛放页面级的组件。react-router
那按照这种层级关系,咱们先来看下BrowersRouter里实现了什么功能。app
import { Router } from "react-router";
import { createBrowserHistory as createHistory } from "history"; class BrowserRouter extends React.Component { history = createHistory(this.props); render() { return <Router history={this.history} children={this.props.children} />; } } 复制代码
很是少许的几行代码,很清晰的看到,核心点是history这个库所提供的函数。组件在render前执行了createHistory这个函数,而后它会返回一个history的对象实例,而后经过props传给Router这个路由器,另外其中包裹的全部子组件,通通传给Router。
这里其实官网上已经说的很清楚,你们用的时候能够多留意下。
那么思路就很清楚,重点放在Router和history库上,看看Router是怎么用这个history对象的,以及这个history对象里又包含了啥,和window.history有什么区别?让咱们接着往下走。
import HistoryContext from "./HistoryContext.js";
import RouterContext from "./RouterContext.js"; 复制代码
Router是核心的路由器,上面咱们已经看到BrowsRouter传递给它了一个history对象。
首先引入了两个context,这里其实就是建立的普通context,只不过拥有特定的名称而已。
它的内部实现是这样
const createNamedContext = name => {
const context = createContext(); context.displayName = name; return context; }; // 上述的引用就至关于 HistoryContext = createNamedContext("Router-History") 复制代码
引入了这两个context后,在来看它的构造函数。
constructor(props) { super(props); this.state = { location: props.history.location }; this.unlisten = props.history.listen(location => { this.setState({ location }); }); } 复制代码
Router组件维护了一个内部状态location对象,初始值为上面提到的在BrowsRouter中建立的history提供的。
以后,执行了history对象提供的listen函数,这个函数须要一个回调函数做为入参,传入的回调函数的功能就是来更新当前Router内部状态中的location的,关于何时会执行这个回调,以及listen函数,后面会详细剖析。
componentWillUnmount() {
if (this.unlisten) { this.unlisten(); } } 复制代码
等这个Router组件将要卸载时,就取消对history的监听。
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> ); } 复制代码
最后生成的react树,就是由最开始引入的context组成的,而后传入history、location这些值。
总结就是整个Router就是一个传入了history、locaiton和其它一些数据的context的提供者,而后它的子组件做为消费者就能够共享使用这些数据,来完成后面的路由跳转、UI更新等动做。
在Router组件能够看到已经用到了createBrowserHistory函数返回的history实例了,如:history.location和history.listen,这个库里的封装的函数那是至关多了,细节也不少,我仍然挑最重要的解读。
首先是我们这个出镜率较高的history提供了哪些属性和方法
看起来都是些熟悉的东西,如push、replace、go这些,都是window对象属性history所提供的。但有些属性实际上是重写了的,如push、replace,其它的是作了一个简单封装。
function goBack() {
go(-1); } function goForward() { go(1); } 复制代码
Router内部状态location的初始数据,是使用window.location与window.history.state作的重组。
路由系统最为重要的两个切换页面动做,一个是push,一个是replace,咱们平时只用Link组件的话,并无确切的感觉,其中Link接受一个props属性,to :string 或者to : object
<link to='/course'>跳转</link>
此时点击它时,调用的就是props.history中重写的push方法。
<Link to='/course' replace>跳转</Link>
若是增长replace属性,则用的就是replace方法
这两个方法主要用的是pushState和replaceState这两个API,它们提供的能力就是能够增长新的window.history中的历史记录和浏览器地址栏上的url,可是又不会发起真正的网络请求。
这是实现单页面应用的关键点。
而后让咱们看一下这两个路由跳转方法
精简后,代码仍是很多,我解读下。
push中的入参path,是接下来准备要跳转的路由地址。createLocation方法先将这个path,与当前的location作一个合并,返回一个更新的loation。
而后就是重头戏,transitionManager这个对象,让咱们先关注下成功回调里面的内容。
经过更新后的location,建立出将要跳转的href,而后调用pushState方法,来更新window.history中的历史记录。
若是你在BrowserRouter中传了forceRefresh这个属性,那么以后就会直接修改window.lcoation.href,来实现页面跳转,但这样就至关于要从新刷新来进行网络请求你的文件资源了。
若是没有传的话,就是调用setState这个函数,注意这个setState并非react提供的那个,而是history库本身实现的。
function setState(nextState) {
history.length = globalHistory.length; transitionManager.notifyListeners(history.location, history.action); } 复制代码
仍是用到了transitionManager对象的一个方法。
另外当咱们执行了pushState后,接下来所获取到的window.history都是已经更新的了。
接下来就剩transitionManager这最后的一个点了。
transitionManager是经过createTransitionManager这个函数实例出的一个对象
function createTransitionManager() {
var listeners = []; function appendListener(fn) { var isActive = true; function listener() { if (isActive) fn.apply(void 0, arguments); } listeners.push(listener); return function () { isActive = false; listeners = listeners.filter(function (item) { return item !== listener; }); }; } function notifyListeners() { for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) { args[_key] = arguments[_key]; } listeners.forEach(function (listener) { return listener.apply(void 0, args); }); } return { appendListener: appendListener, notifyListeners: notifyListeners }; } 复制代码
还记的开始时咱们在Router组件中已经用过一个history.listen方法,其中内部实现就是用的transitionManager.appendListener方法
function listen(listener) {
var unlisten = transitionManager.appendListener(listener); checkDOMListeners(1); return function () { checkDOMListeners(-1); unlisten(); }; } 复制代码
当时咱们给listen传入了一个回调函数,这个回调函数是用来经过React的setState来更新组件内部状态的locaton数据,而后又由于这个lcoation传入了Router-context的value中,因此当它发生变化时,全部的消费组件,都会从新render,以此来达到更新UI的目的。
listen的执行细节是,把它的入参函数(这里指更新Rrouter的state.location的函数)会传入到appendListener中。
执行appendListener后,appendListener将这个入参函数推到listeners这个数组中,保存起来。而后返回一个函数用来删除掉推动该数组的那个函数,以此来实现取消监听的功能。
因此当咱们使用push,切换路由时,它会执行notifyListeners并传入更新的location。
而后就是遍历listeners,执行咱们在listen传入的回调,此时就是最终的去更新Router的location的过程了。
后面的流程,简单说下,Router里面的Route组件经过匹配pathname 和 更新的location ,来决定是否渲染该页面组件,到此整个的路由跳转的过程就结束了。
第一次阅读源码,尽管删减了不少,但仍是写了很多。
但愿你们能够沿着这个思路,本身也去看看,仍是有不少细节值得推敲的。
最后,觉的有帮助,但愿给个赞。