本篇文章主要分析react-router-redux 与react-router 这两个插件的底层源码部分
首先来看一下简单的react-router-redux 来了解一下它的运行机制
如果有什么问题的话 可以加我QQ:469373256
示例
import React from 'react'
import { render } from 'react-dom'
import { connect, Provider } from 'react-redux'
import {
ConnectedRouter,
routerReducer,
routerMiddleware,
push
} from 'react-router-redux'
import { createStore, applyMiddleware, combineReducers } from 'redux'
import createHistory from 'history/createBrowserHistory'
import { Route, Switch } from 'react-router'
import { Redirect } from 'react-router-dom'
const history = createHistory()
const authSuccess = () => ({
type: 'AUTH_SUCCESS'
})
const authFail = () => ({
type: 'AUTH_FAIL'
})
const initialState = {
isAuthenticated: false
}
const authReducer = (state = initialState , action) => {
switch (action.type) {
case 'AUTH_SUCCESS':
return {
...state,
isAuthenticated: true
}
case 'AUTH_FAIL':
return {
...state,
isAuthenticated: false
}
default:
return state
}
}
const store = createStore(
combineReducers({ routerReducer, authReducer }),
applyMiddleware(routerMiddleware(history)),
)
class LoginContainer extends React.Component {
render() {
return
}
}
class HomeContainer extends React.Component {
componentWillMount() {
alert('Private home is at: ' + this.props.location.pathname)
}
render() {
return
}
}
class PrivateRouteContainer extends React.Component {
render() {
const {
isAuthenticated,
component: Component,
...props
} = this.props
return (
isAuthenticated
?
: (
)
}
/>
)
}
}
const PrivateRoute = connect(state => ({
isAuthenticated: state.authReducer.isAuthenticated
}))(PrivateRouteContainer)
const Login = connect(null, dispatch => ({
login: () => {
dispatch(authSuccess())
dispatch(push('/'))
}
}))(LoginContainer)
const Home = connect(null, dispatch => ({
logout: () => {
dispatch(authFail())
dispatch(push('/login'))
}
}))(HomeContainer)
render(
,
document.getElementById('root'),
)
先看一下初始化部分
const history = createHistory()
const store = createStore(
combineReducers({ routerReducer, authReducer }),
applyMiddleware(routerMiddleware(history)),
)
首先在这里定义你想要的history
然后将react-router-redux中对应的reduce以及这个中间件进行一下注入
然后在这里将个刚才注册的history传递给ConnectedRouter用来发起一次dispatch
,
//绑定组件对应的dispatch用来触发一次action
const Login = connect(null, dispatch => ({
login: () => {
dispatch(authSuccess())
dispatch(push('/'))
}
}))(LoginContainer)
const Home = connect(null, dispatch => ({
logout: () => {
dispatch(authFail())
dispatch(push('/login'))
}
}))(HomeContainer)
现在开始一点点来进行一下分析
先来看一下ConnectedRouter部分
import React, { Component } from "react";
import PropTypes from "prop-types";
import { Router } from "react-router";
import { LOCATION_CHANGE } from "./reducer";
class ConnectedRouter extends Component {
static propTypes = {
store: PropTypes.object,
history: PropTypes.object.isRequired,
children: PropTypes.node,
isSSR: PropTypes.bool
};
static contextTypes = {
store: PropTypes.object
};
handleLocationChange = (location, action) => {
this.store.dispatch({
type: LOCATION_CHANGE,
payload: {
location,
action
}
});
};
componentWillMount() {
const { store: propsStore, history, isSSR } = this.props;
this.store = propsStore || this.context.store;
if (!isSSR)
this.unsubscribeFromHistory = history.listen(this.handleLocationChange);
this.handleLocationChange(history.location);
}
componentWillUnmount() {
if (this.unsubscribeFromHistory) this.unsubscribeFromHistory();
}
render() {
return ;
}
}
export default ConnectedRouter;
重点来看一下ConnectedRouter的componentWillMount部分
componentWillMount() {
const { store: propsStore, history, isSSR } = this.props;
this.store = propsStore || this.context.store;
if (!isSSR)
this.unsubscribeFromHistory = history.listen(this.handleLocationChange);
this.handleLocationChange(history.location);
}
从这里可以看到 如果当前不是isSSR服务端渲染的话 那么就会发起一个监听 用来监听当前路由的变化 如果history发生变化时 触发handleLocationChange事件
handleLocationChange事件
handleLocationChange = (location, action) => {
this.store.dispatch({
type: LOCATION_CHANGE,
payload: {
location,
action
}
});
};
这个事件的作用很简单 当路由发生变化 就发起一次dispatch用来重新刷新页面 在这里得来先屡一下这个调用的先后顺序
1.react-router-redux中间件的实现部分
export default function routerMiddleware(history) {
return () => next => action => {
if (action.type !== CALL_HISTORY_METHOD) {
return next(action);
}
const { payload: { method, args } } = action;
history[method](...args);
};
}
从这里可以看到 如果action的类型不为CALL_HISTORY_METHOD就直接放行 让下一个中间件去处理 如果当前类型等于CALL_HISTORY_METHOD则触发history
下面来看一下CALL_HISTORY_METHOD这个究竟是个什么东西
2.Action定义
export const CALL_HISTORY_METHOD = "@@router/CALL_HISTORY_METHOD";
function updateLocation(method) {
return (...args) => ({
type: CALL_HISTORY_METHOD,
payload: { method, args }
});
}
/**
* These actions correspond to the history API.
* The associated routerMiddleware will capture these events before they get to
* your reducer and reissue them as the matching function on your history.
*/
export const push = updateLocation("push");
export const replace = updateLocation("replace");
export const go = updateLocation("go");
export const goBack = updateLocation("goBack");
export const goForward = updateLocation("goForward");
export const routerActions = { push, replace, go, goBack, goForward };
链接上文就可以发现 我们所有调用的push replace的type都是CALL_HISTORY_METHOD
举个简单的例子
this.props.dispatch(push('./'))
那么实际上我们发送的是一个这样的一个action
return (...args) => ({
type: ‘@@router/CALL_HISTORY_METHOD’,
payload: { method:'push', args }
});
在连接上文的代码来看一下
export default function routerMiddleware(history) {
return () => next => action => {
if (action.type !== CALL_HISTORY_METHOD) {
return next(action);
}
const { payload: { method, args } } = action;
history[method](...args);
};
}
这时候 你会发现 这里实际上就变成了
history.push(...args)这种方式去进行了调用
聪明的你 我想应该已经发现这个react-router-redux的运行机制了
当你用push或者replace进行任何操作的时候
最终都会被转换成history中对应的方法
然后因为我们对history进行了操作 所以会触发他对应的回调
通过这种方式来做到了页面的跳转
但是从现有的代码里面 你会发现 貌似没有任何一个地方会导致页面被重新渲染 别急 继续往下看
react-router分析
这里主要介绍Prompt Router Route Redirect
其他的都是一些衍生的产物 就不过多介绍了
先来看一下Router
class Router extends React.Component {
static propTypes = {
history: PropTypes.object.isRequired,
children: PropTypes.node
};
static contextTypes = {
router: PropTypes.object
};
static childContextTypes = {
router: PropTypes.object.isRequired
};
getChildContext() {
return {
router: {
...this.context.router,
history: this.props.history,
route: {
location: this.props.history.location,
match: this.state.match
}
}
};
}
state = {
match: this.computeMatch(this.props.history.location.pathname)
};
computeMatch(pathname) {
return {
path: "/",
url: "/",
params: {},
isExact: pathname === "/"
};
}
componentWillMount() {
const { children, history } = this.props;
invariant(
children == null || React.Children.count(children) === 1,
"A may have only one child element"
);
// Do this here so we can setState when a changes the
// location in componentWillMount. This happens e.g. when doing
// server rendering using a .
this.unlisten = history.listen(() => {
this.setState({
match: this.computeMatch(history.location.pathname)
});
});
}
componentWillReceiveProps(nextProps) {
warning(
this.props.history === nextProps.history,
"You cannot change "
);
}
componentWillUnmount() {
this.unlisten();
}
render() {
const { children } = this.props;
return children ? React.Children.only(children) : null;
}
}
export default Router;
接我们上文讲到的话题 当你发起一个dispatch的时候 为什么页面就会发生变化呢
这里来看一下关键代码
componentWillMount() {
const { children, history } = this.props;
invariant(
children == null || React.Children.count(children) === 1,
"A may have only one child element"
);
// Do this here so we can setState when a changes the
// location in componentWillMount. This happens e.g. when doing
// server rendering using a .
this.unlisten = history.listen(() => {
this.setState({
match: this.computeMatch(history.location.pathname)
});
});
}
看到这里 我想你应该就明白了吧
你会发现 原来Router在这里也对history进行了一个监听
只要你发起了一个dispatch并且正常调用了history以后 这边就会接收到这个更新 并且触发一次setState
这里我们知道 如果父级刷新的时候 所有的children都会进行一次render计算 所以 页面的刷新 其实就是这么来的
是不是比你想象的要简单很多呢
再来看看route部分
class Route extends React.Component {
static propTypes = {
computedMatch: PropTypes.object, // private, from
path: PropTypes.string,
exact: PropTypes.bool,
strict: PropTypes.bool,
sensitive: PropTypes.bool,
component: PropTypes.func,
render: PropTypes.func,
children: PropTypes.oneOfType([PropTypes.func, PropTypes.node]),
location: PropTypes.object
};
static contextTypes = {
router: PropTypes.shape({
history: PropTypes.object.isRequired,
route: PropTypes.object.isRequired,
staticContext: PropTypes.object
})
};
static childContextTypes = {
router: PropTypes.object.isRequired
};
getChildContext() {
return {
router: {
...this.context.router,
route: {
location: this.props.location || this.context.router.route.location,
match: this.state.match
}
}
};
}
state = {
match: this.computeMatch(this.props, this.context.router)
};
computeMatch(
{ computedMatch, location, path, strict, exact, sensitive },
router
) {
if (computedMatch) return computedMatch; // already computed the match for us
invariant(
router,
"You should not use or withRouter() outside a "
);
const { route } = router;
const pathname = (location || route.location).pathname;
return matchPath(pathname, { path, strict, exact, sensitive }, route.match);
}
componentWillMount() {
warning(
!(this.props.component && this.props.render),
"You should not use and in the same route; will be ignored"
);
warning(
!(
this.props.component &&
this.props.children &&
!isEmptyChildren(this.props.children)
),
"You should not use and in the same route; will be ignored"
);
warning(
!(
this.props.render &&
this.props.children &&
!isEmptyChildren(this.props.children)
),
"You should not use and in the same route; will be ignored"
);
}
componentWillReceiveProps(nextProps, nextContext) {
warning(
!(nextProps.location && !this.props.location),
' elements should not change from uncontrolled to controlled (or vice versa). You initially used no "location" prop and then provided one on a subsequent render.'
);
warning(
!(!nextProps.location && this.props.location),
' elements should not change from controlled to uncontrolled (or vice versa). You provided a "location" prop initially but omitted it on a subsequent render.'
);
this.setState({
match: this.computeMatch(nextProps, nextContext.router)
});
}
render() {
const { match } = this.state;
const { children, component, render } = this.props;
const { history, route, staticContext } = this.context.router;
const location = this.props.location || route.location;
const props = { match, location, history, staticContext };
if (component) return match ? React.createElement(component, props) : null;
if (render) return match ? render(props) : null;
if (typeof children === "function") return children(props);
if (children && !isEmptyChildren(children))
return React.Children.only(children);
return null;
}
}
export default Route;
我们在上面已经知道了一个大概的dispatch刷新页面的流程以后
我们这边要继续深入一下 来了解一下大概的刷新逻辑
这里主要是关注几点
1.
componentWillReceiveProps(nextProps, nextContext) {
warning(
!(nextProps.location && !this.props.location),
' elements should not change from uncontrolled to controlled (or vice versa). You initially used no "location" prop and then provided one on a subsequent render.'
);
warning(
!(!nextProps.location && this.props.location),
' elements should not change from controlled to uncontrolled (or vice versa). You provided a "location" prop initially but omitted it on a subsequent render.'
);
this.setState({
match: this.computeMatch(nextProps, nextContext.router)
});
}
这里你会发现 当我们刚才发起一个dispatch的时候 因为父执行了setState以后 导致所有的children都触发了一个更新
这里子就会重新执行computeMatch来判断当前Route这个组件对应的children或者component render等函数是否要执行并且显示对应的页面
Route computeMatch部分
computeMatch(
{ computedMatch, location, path, strict, exact, sensitive },
router
) {
if (computedMatch) return computedMatch; // already computed the match for us
invariant(
router,
"You should not use or withRouter() outside a "
);
const { route } = router;
const pathname = (location || route.location).pathname;
return matchPath(pathname, { path, strict, exact, sensitive }, route.match);
}
Route matchPath路径匹配规则
const matchPath = (pathname, options = {}, parent) => {
//如果options类型为string类型的话 则path变成options
if (typeof options === "string") options = { path: options };
const { path, exact = false, strict = false, sensitive = false } = options;
//如果path为空的时候 则使用this.context的内容
//这里就是404的关键所在
if (path == null) return parent;
const { re, keys } = compilePath(path, { end: exact, strict, sensitive });
const match = re.exec(pathname);
//如果不匹配则直接返回null表示你当前这个组件的route不符合也就不会刷新出来
if (!match) return null;
//分解url
const [url, ...values] = match;
const isExact = pathname === url;
//如果设置为强制匹配 但是实际结果不强制的话 也直接null不刷新显示
if (exact && !isExact) return null;
//一切正常的时候 返回对应的match来刷新页面
return {
path, // the path pattern 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;
}, {})
};
};
export default matchPath;
compilePath 部分代码
const patternCache = {};
const cacheLimit = 10000;
let cacheCount = 0;
const compilePath = (pattern, options) => {
// 这里会将你的参数变成一个文本
const cacheKey = `${options.end}${options.strict}${options.sensitive}`;
//这里会根据传进来的end strict sensitive来进行分类
//如有两条数据
1.end:true,strict:false, sensitive:true
2.end:true,strict:false, sensitive:false
那么就会在patternCache里面保存两条这个数据 并且将这个对应
进行返回
const cache = patternCache[cacheKey] || (patternCache[cacheKey] = {});
这里返回的这个cache也就是指定类的集合 里面放的都是同等类型的
如果cache中有相同的话 就直接返回相同的 不进行其他多余的运算
if (cache[pattern]) return cache[pattern];
首次初始化的时候 这边肯定是为空的 所以这边进行运算 生成一个最新的值
const keys = [];
const re = pathToRegexp(pattern, keys, options);
const compiledPattern = { re, keys };
// 这里要注意 你的每次路由跳转以及初始化都会使用cacheCount
最大值是10000 也就是说 如果超过了10000 则下次进行不会使用cache里面的值 而是每次都进行计算返回最新的数据
if (cacheCount < cacheLimit) {
cache[pattern] = compiledPattern;
cacheCount++;
}
//返回最新的计算结果
return compiledPattern;
};
Route render
我们已经了解了整个router的更新机制 现在来看一下这个是如何被render的
render() {
const { match } = this.state;
const { children, component, render } = this.props;
const { history, route, staticContext } = this.context.router;
const location = this.props.location || route.location;
const props = { match, location, history, staticContext };
只要你的match为true 就会显示出来
但是这里比较特殊的是404那种为匹配到的页面
如果你的props中没有path的话 会返回parent的match
这个时候只要你有component就会直接给你显示出来
if (component) return match ? React.createElement(component, props) : null;
if (render) return match ? render(props) : null;
if (typeof children === "function") return children(props);
if (children && !isEmptyChildren(children))
return React.Children.only(children);
return null;
}
Redirect 部分源码讲解
class Redirect extends React.Component {
static propTypes = {
computedMatch: PropTypes.object, // private, from
push: PropTypes.bool,
from: PropTypes.string,
to: PropTypes.oneOfType([PropTypes.string, PropTypes.object]).isRequired
};
static defaultProps = {
push: false
};
static contextTypes = {
router: PropTypes.shape({
history: PropTypes.shape({
push: PropTypes.func.isRequired,
replace: PropTypes.func.isRequired
}).isRequired,
staticContext: PropTypes.object
}).isRequired
};
//只有你的父级为Route的时候 才会有staticContext
isStatic() {
return this.context.router && this.context.router.staticContext;
}
componentWillMount() {
invariant(
this.context.router,
"You should not use outside a "
);
if (this.isStatic()) this.perform();
}
componentDidMount() {
if (!this.isStatic()) this.perform();
}
componentDidUpdate(prevProps) {
const prevTo = createLocation(prevProps.to);
const nextTo = createLocation(this.props.to);
if (locationsAreEqual(prevTo, nextTo)) {
warning(
false,
`You tried to redirect to the same route you're currently on: ` +
`"${nextTo.pathname}${nextTo.search}"`
);
return;
}
this.perform();
}
computeTo({ computedMatch, to }) {
// 跳转的时候 分为两种
如果有computedMatch的话 说明你有参数要传递
如果没有的话直接使用to的数据
if (computedMatch) {
if (typeof to === "string") {
return generatePath(to, computedMatch.params);
} else {
return {
...to,
pathname: generatePath(to.pathname, computedMatch.params)
};
}
}
return to;
}
perform() {
const { history } = this.context.router;
const { push } = this.props;
const to = this.computeTo(this.props);
//如果push为真的话就push否则替换
if (push) {
history.push(to);
} else {
history.replace(to);
}
}
render() {
return null;
}
}
export default Redirect;
这个比较简单 就不细讲了 看一遍应该就明白了
Prompt 部分源码
这个组件唯一的作用就是在页面改变的时候 去给个提醒
class Prompt extends React.Component {
static propTypes = {
when: PropTypes.bool,
message: PropTypes.oneOfType([PropTypes.func, PropTypes.string]).isRequired
};
static defaultProps = {
when: true
};
//这句话意味着 你这个组件 永远不能是顶层组件 因为如果自己是顶层的话 是不会有context的
static contextTypes = {
router: PropTypes.shape({
history: PropTypes.shape({
block: PropTypes.func.isRequired
}).isRequired
}).isRequired
};
enable(message) {
if (this.unblock) this.unblock();
this.unblock = this.context.router.history.block(message);
}
disable() {
if (this.unblock) {
this.unblock();
this.unblock = null;
}
}
componentWillMount() {
invariant(
this.context.router,
"You should not use outside a "
);
if (this.props.when) this.enable(this.props.message);
}
componentWillReceiveProps(nextProps) {
只有当this.props.when不为空
并且 前后两次显示的message都不一样的时候 才会开启
if (nextProps.when) {
if (!this.props.when || this.props.message !== nextProps.message)
this.enable(nextProps.message);
} else {
this.disable();
}
}
componentWillUnmount() {
this.disable();
}
render() {
return null;
}
}
export default Prompt;
ok 到这里 整个react-router源码就分析完毕 如果有什么问题的话 可以加我QQ:469373256