断点看源码系列:探究React-router内部工作流程
从实际出发搞清楚react-router内部原理
[toc]
前言:最近在看 react-router 的工作原理网上看了一圈文章,发现很多都是介绍了 history 库的原理和 react-router 里面的 BrowserRouter 组件、Router 组件等等,看了原理是大概知道了,但是它的内部是怎么样的一个工作流程还是一脸懵逼,又在找了一圈资料,看完还是觉得具体流程理不太清。想到平时在 js 代码的调试中可以打断点观察各个变量的情况以及程序的流程,react 程序那应该也可以这样调试。
随后写了一个小 demo 测试一下:
准备文件:
App.js 文件
import React, { PureComponent } from 'react'
import { Link, Switch, Route, withRouter } from 'react-router-dom'
// 为了看 history 的源码而引入
import history from 'history'
import Home from './pages/home';
import Profile from './pages/profile';
class App extends PureComponent {
constructor(props) {
super(props);
this.state = {
}
}
render() {
console.log(this.props);
return (
<div>
<Link className='mylink' to="/">首页</Link>
<Link className='mylink' to="/profile">我的</Link>
<button onClick={() => this.jumpToProduct()}>测试</button>
<Switch>
<Route exact path="/" component={Home} />
<Route path="/profile" component={Profile} />
</Switch>
</div>
)
}
jumpToProduct() {
this.props.history.push("/profile");
}
}
export default withRouter(App);
index.js 文件
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import {
BrowserRouter,
} from 'react-router-dom';
ReactDOM.render(
<BrowserRouter>
<App />
</BrowserRouter>
,
document.getElementById('root')
);
profile.js 组件, Home 一样
import React, { PureComponent } from 'react'
export default class Profile extends PureComponent {
render() {
return (
<div>
<h2>Profile</h2>
</div>
)
}
}
到这里就可以开始了我们的流程探究了,在 App.js 文件的 jumpToProduct 里面的一行代码打上断点。
打断点的具体方式是:将 demo 跑起来 -> 打开浏览器的控制台 -> source -> 具体文件 -> 打上断点
jumpToProduct() {
this.props.history.push("/profile");
}
如图:

点击 button 按钮,进行单步调试。然后程序就跳到 node_modules/history/cjs/history.js 的 push 函数里面了。
这是为什么呢?因为 Link 组件里面判断了是用什么方法改变路径的。这里我们模拟的是 push 方法
1. Link 里面的判断代码片段(以下源码均为节选)
// 1. Link 里面的判断代码片段
var props = _extends({}, rest, {
href: href,
navigate: function navigate() {
var location = resolveToLocation(to, context.location);
var isDuplicateNavigation = history.createPath(context.location) === history.createPath(normalizeToLocation(location));
var method = replace || isDuplicateNavigation ? history$1.replace : history$1.push;
method(location);
}
}); // React 15 compat
2. 这里调用的是 history 下的 push 方法
push 方法里面创建一个新的 location 对象,然后通过 globalHistory 方法改变浏览器当前路由,最后通过setState 方法通知 React-Router 更新(这里会跳到 ReactRefreshEntry.js 这个文件),并传递当前的 location 对象
function push(path, state) {
var action = 'PUSH';
/* 2. 创建location对象 */
var location = createLocation(path, state, createKey(), history.location);
/* 3. 确定是否能进行路由转换 */
transitionManager.confirmTransitionTo(location, action, getUserConfirmation, function (ok) {
if (!ok) return;
var href = createHref(location);
var key = location.key,
state = location.state;
if (canUseHistory) {
/* 改变 url */
globalHistory.pushState({
key: key,
state: state
}, null, href);
if (forceRefresh) {
window.location.href = href;
} else {
/* 改变 react-router location对象, 创建更新环境 */
setState({
action: action,
location: location
});
}
}
});
3. history 下的 createTransitionManager 函数
作用是监听判断是否能进行路由转换
/* 3. 确定是否能进行路由转换 */
function createTransitionManager() {
var prompt = null;
function confirmTransitionTo(location, action, getUserConfirmation, callback) {
if (prompt != null) {
var result = typeof prompt === 'function' ? prompt(location, action) : prompt;
if (typeof result === 'string') {
if (typeof getUserConfirmation === 'function') {
getUserConfirmation(result, callback);
}
} else {
// Return false from a transition hook to cancel the transition.
callback(result !== false);
}
} else {
callback(true);
}
}
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() {
listeners.forEach(function (listener) {
return listener.apply(void 0, args);
});
}
}
4. history 中 createBrowserHistory(props) 函数的 setState
/* 通知listener更新,这里通知到 react-router了 */
const setState = (nextState) => {
/* 合并信息 */
Object.assign(history, nextState)
history.length = globalHistory.length
/* 通知每一个listens路由已经发生变化 */
transitionManager.notifyListeners(
history.location,
history.action
)
}
5. 路由匹配
/* 5. 监听并进行路由匹配 */
class Router extends React.Component {
static computeRootMatch(pathname) {
return { path: "/", url: "/", params: {}, isExact: pathname === "/" };
}
constructor(props) {
super(props);
this.state = {
location: props.history.location
};
this._isMounted = false;
this._pendingLocation = null;
if (!props.staticContext) {
this.unlisten = props.history.listen(location => {
if (this._isMounted) {
this.setState({ location });
} else {
this._pendingLocation = location;
}
});
}
}
}
总结:
路由转换的大致流程就是:
- 模拟点击
Link - 调用
history.push方法,通过history.pushState来改变当前url - 接下来触发
history下面的setState方法,产生新的 location 对象, - 然后通知
Router组件更新location - 路由匹配组件中监听更新的消息,找出符合的组件,最后进行页面更新。
纸上得来终觉浅,绝知此事要躬行
还得再多加油 多多 coding 多多看源码