react-router 源码解读
SunShinewyf opened this issue · 0 comments
在项目过程中,对 react-router 这个库了解甚少,只是停留在一些基础的用法层面,源码层面的东西还是云里雾里,对它如何运作的原理也是知之甚少,所以趁着最近修 bug 的空档期深入学习一下。
此文基于 react-router 4 进行讲解,如果对 react-router 还不太熟悉的童鞋可以移步 初探 React Router 4.0 和 react-router 的 API 文档地址 先学习一下。
react-router-dom 和 react-router
react-router-dom 中包含了 web 端所有路由相关的东西,它基于 react-router 进行了一些包装,所以项目中只需要引入 react-router-dom 即可。react-router 4 中强调万物即组件,其暴露的几乎所有东西都是一个 react 组件,正是因为这种 Just Components 的**使得开发者更容易在项目中使用路由。
一张图显示出他们之间的关系:
从上图可知,react-router-dom 中的很多元素都是直接从 react-router 中拿过来的,只是在它的基础上进行了一些功能上的扩展。
源码分析
Router
Router 是创建路由最外面的包裹层组件,有点类似 Provider 的感觉,它的作用是监听路由的变化,从而渲染的页面组件。其源码如下:
class Router extends React.Component {
static propTypes = {
history: PropTypes.object.isRequired,
children: PropTypes.node
};
state = {
match: this.computeMatch(this.props.history.location.pathname)
};
computeMatch(pathname) {
return {
path: "/",
url: "/",
params: {},
isExact: pathname === "/"
};
}
componentWillMount() {
const { children, history } = this.props;
this.unlisten = history.listen(() => {
this.setState({
match: this.computeMatch(history.location.pathname)
});
});
}
componentWillUnmount() {
this.unlisten();
}
render() {
const { children } = this.props;
return children ? React.Children.only(children) : null;
}
}
源码部分中包含声明接受的 props 数据为 history 对象和要显示的 child 节点内容。并且在组件渲染之前(componentWillMount) 的时候,监听路由( history 是一个记录浏览器记录的一个库)的改变,当 url 发生更改时可以执行传递过去的 setState 回调,当 state 进行更新时,就会重新执行 render 对应的逻辑,从而引发页面的重新渲染。在 Router 组件销毁期间,取消对浏览器的监听。
history 有三种形式,分别是:
- browser history
- hash history
- memory history
关于 history 的一些基础知识,可以移步 React Router预备知识,关于history的那些事 进行学习,这里不赘述。有三种 history, 对应的就有三种 Router: - BrowserRouter
- HashRouter
- MemoryRouter
这几种 Router 的源码只是改变了 history 的类型,然后传递给 Router 进行展示,以 BrowserRouter 为例,代码如下:
import { createBrowserHistory as createHistory } from "history";
class BrowserRouter extends React.Component {
static propTypes = {
basename: PropTypes.string,
forceRefresh: PropTypes.bool,
getUserConfirmation: PropTypes.func,
keyLength: PropTypes.number,
children: PropTypes.node
};
history = createHistory(this.props);
render() {
return <Router history={this.history} children={this.props.children} />;
}
}
内容很简单,结合 Router 源码很好理解。HashRouter 和 MemoryRouter 的套路一样,在此不再赘述。
Route
Route 是嵌在 Router 里面的元素,如下:
<Router history={browserHistory}>
<Route path="/" component={App}>
<Route path="about" component={About}/>
<Route path="users" component={Users}>
<Route path="/user/:userId" component={User}/>
</Route>
</Route>
</Router>
它的作用是根据 url 进行匹配,如果匹配到当前路由,就展示对应的组件或者内容,否则显示 null。主要源码如下:
class Route extends React.Component {
static propTypes = {
computedMatch: PropTypes.object, // private, from <Switch>
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
};
state = {
match: this.computeMatch(this.props, this.context.router)
};
computeMatch(
{ computedMatch, location, path, strict, exact, sensitive },
router
) {
if (computedMatch) return computedMatch; // <Switch> already computed the match for us
const { route } = router;
const pathname = (location || route.location).pathname;
return matchPath(pathname, { path, strict, exact, sensitive }, route.match);
}
componentWillReceiveProps(nextProps, nextContext) {
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;
}
}
Route 在 componentWillReceiveProps 这个生命周期通过接受父级组件传来的 router 数据,从而来不断更新自己的 state 数据,进而达到重新渲染组件的效果。
其中 matchPath 这个方法,主要是用 path-to-regexp 这个库来匹配路由参数,并且将已经访问过的路由进行缓存,下次再匹配到这个路由的时候直接返回匹配的数据,其中缓存限制为 10000,具体的逻辑可以看 matchPath 源码,这里不贴出。
其中, strict, exact, sensitive 只是限定了一些匹配的规则,然后在 render 中进行渲染对应的渲染函数,根据传入的渲染规则不同,执行不同的渲染方式。
Link
Link 在浏览器中渲染出来就是一个 a 标签,只是动态赋给 a 一个 href 进行页面跳转,源码如下:
class Link extends React.Component {
static propTypes = {
onClick: PropTypes.func,
target: PropTypes.string,
replace: PropTypes.bool,
to: PropTypes.oneOfType([PropTypes.string, PropTypes.object]).isRequired,
innerRef: PropTypes.oneOfType([PropTypes.string, PropTypes.func])
};
handleClick = event => {
if (this.props.onClick) this.props.onClick(event);
if (
!event.defaultPrevented && // onClick prevented default
event.button === 0 && // ignore everything but left clicks
!this.props.target && // let browser handle "target=_blank" etc.
!isModifiedEvent(event) // ignore clicks with modifier keys
) {
event.preventDefault();
const { history } = this.context.router;
const { replace, to } = this.props;
if (replace) {
history.replace(to);
} else {
history.push(to);
}
}
};
render() {
const { replace, to, innerRef, ...props } = this.props; // eslint-disable-line no-unused-vars
const { history } = this.context.router;
const location =
typeof to === "string"
? createLocation(to, null, null, history.location)
: to;
const href = history.createHref(location);
return (
<a {...props} onClick={this.handleClick} href={href} ref={innerRef} />
);
}
}
如上所示:渲染一个 a 标签,并获取一个 href,当点击的时候,触发点击的回调函数,为了防止页面刷新,需要禁掉浏览器的默认行为,所以在 handleClick 中执行了 event.preventDefault(),并根据传进的 props 是 to 还是 replace 进行不同的操作。
NavLink
NavLink 是基于 Link 的一个特殊版本,会在匹配当前 url 的元素添加一些传递的属性和样式:activeClassName 和 activeStyle ,源码部分是 Link 和 Route 的组合版本,这里就不深入了。
Redirect
Redirect 在渲染时将会跳转到一个新的 url,这个新的 url 将会覆盖掉历史信息里面本该访问的那个地址。源码如下:
class Redirect extends React.Component {
static propTypes = {
computedMatch: PropTypes.object, // private, from <Switch>
push: PropTypes.bool,
from: PropTypes.string,
to: PropTypes.oneOfType([PropTypes.string, PropTypes.object]).isRequired
};
isStatic() {
return this.context.router && this.context.router.staticContext;
}
componentWillMount() {
if (this.isStatic()) this.perform();
}
componentDidMount() {
if (!this.isStatic()) this.perform();
}
componentDidMount() {
if (!this.isStatic()) this.perform();
}
componentDidUpdate(prevProps) {
//省略校验
this.perform();
}
computeTo({ 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);
if (push) {
history.push(to);
} else {
history.replace(to);
}
}
render() {
return null;
}
}
如上源码,Redirect 在组件的几个生命周期中都去执行了 perform 函数,perform 的功能就是通过判断是跳转路由还是覆盖路由从而进行 history 的相应操作,computeTo 功能主要是返回一个 url 进行页面跳转或者替换。和 matchPath 方法类似, computeTo 里调用的 generatePath 也有缓存的操作,对新产生的路由进行缓存,下次再进行 redirect 的时候直接返回,不需要再次通过 pathToRegexp 构造。
Switch
Switch 用来嵌套在 Route 外面,找出第一个匹配的 Route 进行渲染,其他的 Route 就不会去渲染。源码如下:
class Switch extends React.Component {
static propTypes = {
children: PropTypes.node,
location: PropTypes.object
};
render() {
const { route } = this.context.router;
const { children } = this.props;
const location = this.props.location || route.location;
let match, child;
React.Children.forEach(children, element => {
if (match == null && React.isValidElement(element)) {
const {
path: pathProp,
exact,
strict,
sensitive,
from
} = element.props;
const path = pathProp || from;
child = element;
match = matchPath(
location.pathname,
{ path, exact, strict, sensitive },
route.match
);
}
});
return match
? React.cloneElement(child, { location, computedMatch: match })
: null;
}
}
首先会对 Children 进行遍历,然后使用上文提到的 matchPath 方法进行匹配,然后在 render 时只渲染匹配的那个 child。
总结
整个 react-router 的源码还是比较简单的,没有什么特别晦涩的地方,基本上都能看懂,了解其内部源码的原理,在项目中就可以比较得心应手地使用了~