上篇文章介绍了前端路由的两种实现原理,今天我想从react-router
源码分析下他们是如何管理前端路由的。由于以前一直都是使用V4的版本,因此接下来分析的也是基于react-router v4.4.0版本
的(如下简称 V4),欢迎你们提出评论交流。Let's get started。前端
在分析源码前先回顾如下相关知识,有利于更好的理解源码设计。react
react
中如何实现不一样的路由渲染不一样的组件?react 做为一个前端视图框架,自己是不具备除了
view
(数据与界面之间的抽象)以外的任何功能的;上篇文章中咱们是经过触发的回调函数来操做DOM
,而在 react 中咱们不直接操做DOM
,而是管理抽象出来的VDOM
或者说JSX
,对 react 的来讲路由须要管理组件的生命周期
,对不一样的路由渲染不一样的组件。git
history
(第三方库)的使用由于React-Router 是基于
history
这个库来实现对路由变化的监听,因此咱们下面会先对这个库进行简单的分析。固然咱们主要分析它的监听模式listen
是如何实现的,这对实现路由是相当重要的,想了解更多其余的API,请移步history学习更多。github
history
基本用法是这样的:react-native
import { createBrowserHistory } from 'history';
const history = createBrowserHistory();
// 获取当前location。
const location = history.location;
// 监听当前location的更改。
const unlisten = history.listen((location, action) => {
// location是一个相似window.location的对象
console.log(action, location.pathname, location.state);
});
// 使用push、replace和go来导航。
history.push('/home', { some: 'state' });
// 若要中止监听,请调用listen()返回的函数.
unlisten();
复制代码
咱们查看源码modules下面的index.js,能够看出history 暴露出了七个方法:数组
export { default as createBrowserHistory } from './createBrowserHistory';
export { default as createHashHistory } from './createHashHistory';
export { default as createMemoryHistory } from './createMemoryHistory';
export { createLocation, locationsAreEqual } from './LocationUtils';
export { parsePath, createPath } from './PathUtils';
复制代码
经过上面的例子咱们来简单比较createBrowserHistory
和createHashHistory
:浏览器
createHashHistory
使用 URL 中的 hash(#)
部分去建立形如 example.com/#/some/path
的路由。
createHashHistory源码简析:react-router
function getHashPath() {
// 咱们不能使用window.location.hash,由于它不是跨浏览器一致- Firefox将预解码它!
const href = window.location.href;
const hashIndex = href.indexOf('#');
return hashIndex === -1 ? '' : href.substring(hashIndex + 1);//函数返回hush值
}
复制代码
createBrowserHistory
使用浏览器中的 History API 用于处理 URL,建立一个像example.com/some/path
这样真实的 URL 。
createBrowserHistory.js源码简析:app
//createBrowserHistory.js
const PopStateEvent = 'popstate';//变量,下面window监听事件popstate用到
const HashChangeEvent = 'hashchange';//变量,下面window监听事件hashchange用到
function createBrowserHistory(props = {}) {
invariant(canUseDOM, 'Browser history needs a DOM');
const globalHistory = window.history; // 建立一个使用HTML5 history API(包括)的history对象
const canUseHistory = supportsHistory();
const needsHashChangeListener = !supportsPopStateOnHashChange();
//...
//push方法
function push(path, state) {
if (canUseHistory) {
globalHistory.pushState({ key, state }, null, href);//在push方法内使用pushState
...
}
//replace方法
function replace(path, state) {
if (canUseHistory) {
globalHistory.replaceState({ key, state }, null, href);//在replaceState方法内使用replaceState
}
}
let listenerCount = 0;
//注册路由监听事件
function checkDOMListeners(delta) {
listenerCount += delta;
if (listenerCount === 1 && delta === 1) {
window.addEventListener(PopStateEvent, handlePopState);//popstate监听前进/后退事件
if (needsHashChangeListener)
window.addEventListener(HashChangeEvent, handleHashChange);//hashchange监听 URL 的变化
} else if (listenerCount === 0) {
window.removeEventListener(PopStateEvent, handlePopState);
if (needsHashChangeListener)
window.removeEventListener(HashChangeEvent, handleHashChange);
}
}
//路由监听
function listen(listener) {
const unlisten = transitionManager.appendListener(listener);
checkDOMListeners(1);
return () => {
checkDOMListeners(-1);
unlisten();
};
}
复制代码
listen如何触发监听:框架
上面
createBrowserHistory.js
中也有介绍如何注册路由监听,咱们再看下如何触发路由监听者。 分析事件监听的回调函数handlePopState
,其最终是经过setState
来触发路由监听者,其中notifyListeners
会调用全部的listen
的回调函数,从而达到通知监听路由变化的监听者。
//createBrowserHistory.js
const transitionManager = createTransitionManager();
function setState(nextState) {
Object.assign(history, nextState);
history.length = globalHistory.length;
// 调用全部的listen 的回调函数,从而达到通知监听路由变化的监听者
transitionManager.notifyListeners(history.location, history.action);
}
//事件监听的回调函数handlePopState
function handlePopState(event) {
// 忽略WebKit中无关的popstate事件。
if (isExtraneousPopstateEvent(event)) return;
handlePop(getDOMLocation(event.state));
}
let forceNextPop = false;
//回调执行函数
function handlePop(location) {
if (forceNextPop) {
forceNextPop = false;
setState();
} else {
const action = 'POP';
transitionManager.confirmTransitionTo(
location,
action,
getUserConfirmation,
ok => {
if (ok) {
setState({ action, location });
} else {
revertPop(location);
}
}
);
}
}
复制代码
// createTransitionManager.js
function notifyListeners(...args) {
//调用全部的listen 的回调函数
listeners.forEach(listener => listener(...args));
}
复制代码
在react-router
的Router
组件的componentWillMount 生命周期中就调用了history.listen调用,从而达到当路由变化, 会去调用setState 方法, 从而去Render 对应的路由组件。
// react-router/Router.js
constructor(props) {
super(props);
this.state = {
location: props.history.location
};
//调用history.lesten方法监听,setState渲染组件
this.unlisten = props.history.listen(location => {
this.setState({ location });
});
}
componentWillUnmount() {
//路由监听
this.unlisten();
}
复制代码
小结:以上分析了react-router
如何使用第三方库history
监听路由变化的过程,下面将介绍react-router
是如何结合history
作到SPA
路由变化达到渲染不一样组件的效果的,咱们先看下react-router
的基本使用,梳理解析思路。
//引用官方例子
import React from "react";
import { BrowserRouter as Router, Route, Link } from "react-router-dom";
function BasicExample() {
return (
<Router>
<div>
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/about">About</Link>
</li>
<li>
<Link to="/topics">Topics</Link>
</li>
</ul>
<hr />
<Route exact path="/" component={Home} />
<Route path="/about" component={About} />
<Route path="/topics" component={Topics} />
</div>
</Router>
);
}
function Home() {
return (
<div>
<h2>Home</h2>
</div>
);
}
function About() {
return (
<div>
<h2>About</h2>
</div>
);
}
function Topics({ match }) {
return (
<div>
<h2>Topics</h2>
<ul>
<li>
<Link to={`${match.url}/rendering`}>Rendering with React</Link>
</li>
<li>
<Link to={`${match.url}/components`}>Components</Link>
</li>
<li>
<Link to={`${match.url}/props-v-state`}>Props v. State</Link>
</li>
</ul>
<Route path={`${match.path}/:topicId`} component={Topic} />
<Route
exact
path={match.path}
render={() => <h3>Please select a topic.</h3>}
/>
</div>
);
}
function Topic({ match }) {
return (
<div>
<h3>{match.params.topicId}</h3>
</div>
);
}
export default BasicExample;
复制代码
V4
将路由拆成了如下几个包:
react-router
负责通用的路由逻辑react-router-dom
负责浏览器的路由管理react-router-native
负责 react-native 的路由管理用户只需引入 react-router-dom
或 react-router-native
便可,react-router
做为依赖存在再也不须要单独引入。
React Router
中有三种类型的组件:
上面Demo中咱们也正使用了这三类组件,为了方便分析源码,咱们能够梳理一个基本流程出来:
<BrowserRouter>
建立一个专门的history对象,并注册监听事件。<Route>
匹配的path,并渲染匹配的组件。<Link>
建立一个连接跳转到你想要渲染的组件。下面咱们就根据上述流程步骤,一步一步解析react-router
代码实现。
/* react-router-dom/BrowserRouter.js */
//从history引入createBrowserHistory
import { createBrowserHistory as createHistory } from "history";
import warning from "warning";
//导入react-router 的 Router 组件
import Router from "./Router";
class BrowserRouter extends React.Component {
//建立全局的history对象,这里用的是HTML5 history
history = createHistory(this.props);
render() {
//将 history 做为 props 传递给 react-router 的 Router 组件
return <Router history={this.history} children={this.props.children} />; } } 复制代码
BrowserRouter
的源码在 react-router-dom
中,它是一个高阶组件,在内部建立一个全局的 history
对象(能够监听整个路由的变化),并将 history
做为 props 传递给 react-router
的 Router
组件(Router 组件再会将这个 history 的属性做为 context 传递给子组件)。以下,借助 context 向 Route 传递组件,这也解释了为何 Router 要在全部 Route 的外面。
//react-router/Router.js
import React from "react";
import RouterContext from "./RouterContext";
//获取history、location、match...
function getContext(props, state) {
return {
history: props.history,
location: state.location,
match: Router.computeRootMatch(state.location.pathname),
staticContext: props.staticContext
};
}
class Router extends React.Component {
//定义Router组件的match属性字段
static computeRootMatch(pathname) {
return { path: "/", url: "/", params: {}, isExact: pathname === "/" };
/* * path: "/", // 用来匹配的 path * url: "/", // 当前的 URL * params: {}, // 路径中的参数 * isExact: pathname === "/" // 是否为严格匹配 */
}
constructor(props) {
super(props);
this.state = {
location: props.history.location
};
//监听路由的变化并执行回调事件,回调内setState
this.unlisten = props.history.listen(location => {
this.setState({ location });
/* *hash: "" // hash *key: "nyi4ea" // 一个 uuid *pathname: "/explore" // URL 中路径部分 *search: "" // URL 参数 *state: undefined // 路由跳转时传递的 state */
});
}
componentWillUnmount() {
//组件卸载时中止监听
this.unlisten();
}
render() {
const context = getContext(this.props, this.state);
return (
<RouterContext.Provider children={this.props.children || null} value={context}<!--借助 context 向 Route 传递组件--> /> ); } } export default Router; 复制代码
相比于在监听的回调里setState
作的操做,setState
自己的意义更大 —— 每次路由变化 -> 触发顶层 Router
的回调事件 -> Router
进行 setState
-> 向下传递 nextContext
(context
中含有最新的 location
)-> 下面的 Route
获取新的 nextContext
判断是否进行渲染。
源码中有这样介绍:"用于匹配单个路径和呈现的公共API"。简单理解为找到
location
和<router>
的path
匹配的组件并渲染。
//react-router/Route.js
//判断Route的子组件是否为空
function isEmptyChildren(children) {
return React.Children.count(children) === 0;
}
//获取history、location、match...
//父组件没传的话使用context中的
function getContext(props, context) {
const location = props.location || context.location;
const match = props.computedMatch
? props.computedMatch // <Switch> already computed the match for us
: props.path
? matchPath(location.pathname, props)//在./matchPath.js中匹配location.pathname与path
: context.match;
return { ...context, location, match };
}
class Route extends React.Component {
render() {
return (
<RouterContext.Consumer>
{context => {
invariant(context, "You should not use <Route> outside a <Router>");
const props = getContext(this.props, context);
//context 更新 props 和 nextContext会从新匹配
let { children, component, render } = this.props;
// 提早使用一个空数组做为children默认值,若是是这样,就使用null。
if (Array.isArray(children) && children.length === 0) {
children = null;
}
if (typeof children === "function") {
children = children(props);
if (children === undefined) {
children = null;
}
}
return (
<RouterContext.Provider value={props}>
{children && !isEmptyChildren(children)
? children
: props.match//对应三种渲染方式children、component、render,只能使用一种
? component
? React.createElement(component, props)
: render
? render(props)
: null
: null}
</RouterContext.Provider>
);
}}
</RouterContext.Consumer>
);
}
}
复制代码
Route
接受上层的 Router
传入的 context
,Router
中的 history 监听着整个页面的路由变化,当页面发生跳转时,history
触发监听事件,Router
向下传递 nextContext
,就会更新 Route
的 props
和 context
来判断当前 Route
的 path
是否匹配 location
,若是匹配则渲染,不然不渲染。
是否匹配的依据就是 matchPath
这个函数,在下文会有分析,这里只须要知道匹配失败则 match
为 null
,若是匹配成功则将 match
的结果做为 props
的一部分,在 render 中传递给传进来的要渲染的组件。
从render 方法能够知道有三种渲染组件的方法(children
、component
、render
)渲染的优先级也是依次按照顺序,若是前面的已经渲染后了,将会直接 return。
children
(若是children
是一个方法, 则执行这个方法, 若是只是一个子元素,则直接render
这个元素)component
(直接传递一个组件, 而后去render
组件)render
(render 是一个方法, 经过方法去render
这个组件)接下来咱们看下 matchPath
是如何判断 location
是否符合 path 的。
function matchPath(pathname, options = {}) {
if (typeof options === "string") options = { path: options };
const { path, exact = false, strict = false, sensitive = false } = options;
const { regexp, keys } = compilePath(path, { end: exact, strict, sensitive });
const match = regexp.exec(pathname);
if (!match) return null;
const [url, ...values] = match;
const isExact = pathname === url;
if (exact && !isExact) return null;
return {
path, // 用来进行匹配的路径,实际上是直接导出的传入 matchPath 的 options 中的 path
url: path === "/" && url === "" ? "/" : url, // URL的匹配部分
isExact, // url 与 path 是不是 exact 的匹配
// 返回的是一个键值对的映射
// 好比你的 path 是 /users/:id,而后匹配的 pathname 是 /user/123
// 那么 params 的返回值就是 {id: '123'}
params: keys.reduce((memo, key, index) => {
memo[key.name] = values[index];
return memo;
}, {})
};
}
复制代码
class Link extends React.Component {
static defaultProps = {
replace: false
};
handleClick(event, context) {
if (this.props.onClick) this.props.onClick(event);
if (
!event.defaultPrevented && // 阻止默认事件
event.button === 0 && // 忽略除左击以外的全部内容
!this.props.target && // 让浏览器处理“target=_blank”等。
!isModifiedEvent(event) // 忽略带有修饰符键的单击
) {
event.preventDefault();
const method = this.props.replace
? context.history.replace
: context.history.push;
method(this.props.to);
}
}
render() {
const { innerRef, replace, to, ...props } = this.props;
// eslint-disable-line no-unused-vars
return (
<RouterContext.Consumer>
{context => {
invariant(context, "You should not use <Link> outside a <Router>");
const location =
typeof to === "string"
? createLocation(to, null, null, context.location)
: to;
const href = location ? context.history.createHref(location) : "";
return (
<a
{...props}
onClick={event => this.handleClick(event, context)}
href={href}
ref={innerRef}
/>
);
}}
</RouterContext.Consumer>
);
}
}
复制代码
从render来看,
Link
其实就是一个<a>标签,在handleClick
中,对没有被preventDefault
的 && 鼠标左键点击的 && 非 _blank 跳转 的&& 没有按住其余功能键的单击进行preventDefault
,而后push
进history
中,这也是前面讲过的 —— 路由的变化 与 页面的跳转 是不互相关联的,V4
在Link
中经过history
库的push
调用了HTML5 history
的pushState
,可是这仅仅会让路由变化,其余什么都没有改变。还记不记得 Router 中的 listen,它会监听路由的变化,而后经过context
更新props
和nextContext
让下层的Route
去从新匹配,完成须要渲染部分的更新。
让咱们回想下咱们看完基础用法梳理的流程:
<BrowserRouter>
建立一个专门的history对象,并注册监听事件。<Route>
匹配的path,并渲染匹配的组件。<Link>
建立一个连接跳转到你想要渲染的组件。结合源码咱们再分析下具体实现
BrowserRouter
render一个Router
时建立了一个全局的history
对象,并经过props
传递给了Router
,而在Router
中设置了一个监听函数,使用的是history库的listen,触发的回调里面进行了setState
向下传递 nextContext。Link
是,实际上是点击的a
标签,只不过使用了 preventDefault
阻止 a
标签的页面跳转;经过给a
标签添加点击事件去执行 hitsory.push(to)
。Router
的 setState
的,在 Router
那章有写道:每次路由变化 -> 触发顶层 Router
的监听事件 -> Router
触发 setState
-> 向下传递新的 nextContext
(nextContext
中含有最新的 location
)。