从例子出发,看源码是如何处理的
ps. 源码均进行了删减,将判断、报错等部分代码移除;文章比较长,有兴趣的同学可以阅读,也可以直接跳到总结直接了解 React Router 的实现原理
例子
import React from 'react';
import {
BrowserRouter as Router,
Switch,
Route,
Link
} from 'react-router-dom';
export default () => (
<Router>
<div>
<nav>
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/about">About</Link>
</li>
</ul>
</nav>
<Switch>
<Route path="/about">
About
</Route>
<Route path="/">
Home
</Route>
</Switch>
</div>
</Router>
)
对应的 React 结构为
下面我们从各个组件的源码解读
BrowserRouter
- 源码中的写法类似于 ES6 Class 继承的写法,就先不关注这个点了,这边就先将源码直接简化为
class写法(下同) - 这个组件主要是为了传递
history方法,这是整个路由实现的关键方法
// 简单理解为
class BrowserRouter extends React.Componet {
constructor(props) {
super();
his.props = props;
this.history = createBrowserHistory(_this.props);
}
render() {
return React.createElement(Router, {
history: this.history,
children: this.props.children
});
}
}
createBrowserHistory
其中,需要重点关注下 createBrowserHistory 这个函数,它封装了 history 的相关方法
function createBrowserHistory(props) {
// ...
return {
length: globalHistory.length,
action: 'POP',
location: initialLocation,
createHref: createHref,
push: push,
replace: replace,
go: go,
goBack: goBack,
goForward: goForward,
block: block,
listen: listen
};
}
挑几个之后会用到的方法,简单描述下
location
对应的值是 initialLocation,顾名思义,就是初始地址,返回了一个对象,包含路径中的 pathname、hash、search
location: initialLocation,
transitionManager
setPrompt/confirmTransitionTosetPrompt:给prompt赋值confirmTransitionTo:根据prompt的值,判断是否劫持默认的回调函数,执行自定义的getUserConfirmation方法
appendListener/notifyListeners:相当于观察者模式;- 调用
appendListener添加观察者; - 调用
notifyListeners依次触发观察者方法
- 调用
var transitionManager = createTransitionManager();
function createTransitionManager() {
var prompt = null;
function setPrompt(nextPrompt) {
prompt = nextPrompt;
return function () {
if (prompt === nextPrompt) 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 {
callback(true);
}
} else {
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() {
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 {
setPrompt: setPrompt,
confirmTransitionTo: confirmTransitionTo,
appendListener: appendListener,
notifyListeners: notifyListeners
};
}
ps. setPrompt 一般只有在服务端渲染或者调用 <Prompt> 组件的时候才会用到,所以在本例中 confirmTransitionTo 只会调用 callback,接下去的说明也只考虑这种情况
setState
- 合并
history、nectState属性 transitionManager.notifyListeners触发监听者函数
function setState(nextState) {
_extends(history, nextState);
history.length = globalHistory.length;
transitionManager.notifyListeners(history.location, history.action);
}
listen
listen:transitionManager.appendListener添加观察者checkDOMListeners: 监听 / 移除popstate事件(监听用户在浏览器点击后退、前进,或者在js中调用histroy.back(),history.go(),history.forward()等,但监听不到pushState、replaceState方法)needsHashChangeListener:为了兼容hash改变不触发popstate事件的浏览器,所以需要额外增加了监听hashchange事件(本例也暂时不考虑这种情况)handlePopState:相当于执行了setState方法
unlisten:移除transitionMagager中对应的listener方法
function listen(listener) {
var unlisten = transitionManager.appendListener(listener);
checkDOMListeners(1);
return function () {
checkDOMListeners(-1);
unlisten();
};
}
// checkDomListeners
var listenerCount = 0;
function checkDOMListeners(delta) {
listenerCount += delta;
if (listenerCount === 1 && delta === 1) {
window.addEventListener('popstate', handlePopState);
if (needsHashChangeListener) window.addEventListener('hashChange', handleHashChange);
} else if (listenerCount === 0) {
window.removeEventListener('popstate', handlePopState);
if (needsHashChangeListener) window.removeEventListener('hashChange', handleHashChange);
}
}
function handlePopState(event) {
// Ignore extraneous popstate events in WebKit.
if (isExtraneousPopstateEvent(event)) return;
handlePop(getDOMLocation(event.state));
}
var forceNextPop = false;
function handlePop(location) {
if (forceNextPop) {
forceNextPop = false;
setState();
} else {
var action = 'POP';
transitionManager.confirmTransitionTo(location, action, getUserConfirmation, function (ok) {
if (ok) {
setState({
action: action,
location: location
});
} else {
revertPop(location); // 本例流程不会走到这一步,所以 forceNextPop 的状态也不会发生变化
}
});
}
}
push / replace
- 分别对应
history.pushState和history.replaceState方法 - 再根据是否需要强制刷新,来判断是调用
window.location.href = href强制刷新,还是调用setState方法
function push(path, state) {
var action = 'PUSH';
var location = createLocation(path, state, createKey(), history.location);
transitionManager.confirmTransitionTo(location, action, getUserConfirmation, function (ok) {
if (!ok) return;
var href = createHref(location);
var key = location.key,
state = location.state;
if (canUseHistory) { // 判断是否支持 history API
globalHistory.pushState({
key: key,
state: state
}, null, href);
if (forceRefresh) {
window.location.href = href;
} else {
var prevIndex = allKeys.indexOf(history.location.key);
var nextKeys = allKeys.slice(0, prevIndex + 1);
nextKeys.push(location.key);
allKeys = nextKeys;
setState({
action: action,
location: location
});
}
} else {
window.location.href = href;
}
});
}
function replace(path, state) {
var action = 'REPLACE';
var location = createLocation(path, state, createKey(), history.location);
transitionManager.confirmTransitionTo(location, action, getUserConfirmation, function (ok) {
if (!ok) return;
var href = createHref(location);
var key = location.key,
state = location.state;
if (canUseHistory) {
globalHistory.replaceState({
key: key,
state: state
}, null, href);
if (forceRefresh) {
window.location.replace(href);
} else {
var prevIndex = allKeys.indexOf(history.location.key);
if (prevIndex !== -1) allKeys[prevIndex] = location.key;
setState({
action: action,
location: location
});
}
} else {
window.location.replace(href);
}
});
}
回归正题,看 React Route 的组件中,是如何调用上述接口,实现路由的
Router
staticContext:这个只有在使用staticRouter的时候才会用到,所以此时会调用history中的listen方法listen:添加观察者方法,当监听到popState事件后,就会触发该方法(更新state的值);当组件从DOM中移除时,该方法会被移除- 新建
context对象,将state.location、history作为参数进行传递
var context = createNamedContext$1("Router");
class Router extends React.Component {
constructor(props) {
super();
this.props = props;
this.state = {
location: this.props.history.location
}
this._isMounted = false;
this._pendingLocation = null;
if (!this.props.staticContext) {
this.unlisten = props.history.listen(function (location) {
if (this._isMounted) {
this.setState({
location: location
});
} else {
this._pendingLocation = location;
}
});
}
}
static computeRootMatch(pathname) {
return {
path: "/",
url: "/",
params: {},
isExact: pathname === "/"
};
}
componentDidMount() {
this._isMounted = true;
if (this._pendingLocation) {
this.setState({
location: this._pendingLocation
});
}
};
componentWillUnmount() {
if (this.unlisten) this.unlisten();
};
render() {
return React.createElement(context.Provider, {
value: {
history: this.props.history,
location: this.state.location,
match: Router.computeRootMatch(this.state.location.pathname),
staticContext: this.props.staticContext
}
}, React.createElement(historyContext.Provider, {
children: this.props.children || null,
value: this.props.history
}));
}
}
Link
- 根据
React版本,使用forWardRef或ref回调,将DOM Refs暴露给父组件 - 创建
<context.Consumer>组件,传递navigate方法(即history中的push或replace方法)
var Link = forwardRef(function (_ref2, forwardedRef) {
var _ref2$component = _ref2.component,
component = _ref2$component === void 0 ? LinkAnchor : _ref2$component,
replace = _ref2.replace,
to = _ref2.to,
innerRef = _ref2.innerRef,
rest = _objectWithoutPropertiesLoose(_ref2, ["component", "replace", "to", "innerRef"]);
return React.createElement(__RouterContext.Consumer, null, function (context) {
var history = context.history;
var location = normalizeToLocation(resolveToLocation(to, context.location), context.location); // 拼接 to 中的链接地址
var href = location ? history.createHref(location) : "";
var props = _extends({}, rest, {
href: href,
navigate: function navigate() {
var location = resolveToLocation(to, context.location);
var method = replace ? history.replace : history.push;
method(location);
}
}); // React 15 compat
if (forwardRefShim !== forwardRef) {
props.ref = forwardedRef || innerRef;
} else {
props.innerRef = innerRef;
}
return React.createElement(component, props);
});
});
LinkAnchor
本例中没有在 <Link> 中传递 component 属性,所以使用默认的 <LinkAnchor> 组件
- 创建
<a>标签 - 添加
click事件,阻止<a>的默认跳转 - 如果没有自定义
onclick方法的话,则默认使用<Link>组件中的navigate方法(即history中的push或replace方法)
var LinkAnchor = forwardRef(function (_ref, forwardedRef) {
var innerRef = _ref.innerRef,
navigate = _ref.navigate,
_onClick = _ref.onClick,
rest = _objectWithoutPropertiesLoose(_ref, ["innerRef", "navigate", "onClick"]);
var target = rest.target;
var props = _extends({}, rest, {
onClick: function onClick(event) {
try {
if (_onClick) _onClick(event);
} catch (ex) {
event.preventDefault();
throw ex;
}
if (!event.defaultPrevented && // onClick prevented default
event.button === 0 && ( // ignore everything but left clicks
!target || target === "_self") && // let browser handle "target=_blank" etc.
!isModifiedEvent(event) // ignore clicks with modifier keys
) {
event.preventDefault();
navigate();
}
}
}); // React 15 compat
if (forwardRefShim !== forwardRef) {
props.ref = forwardedRef || innerRef;
} else {
props.ref = innerRef;
}
/* eslint-disable-next-line jsx-a11y/anchor-has-content */
return React.createElement("a", props);
});
Switch
React.Children.forEach:相当于在遍历<Switch>的子元素,即所有的<Route>,取出与路由匹配的<Route>,克隆该组件进行显示- 如果在
<Switch>组件上定义了location变量,则他就会忽略context上传递过来的location的变化,就只会渲染匹配的<Route>
class Switch extends React.Component {
constructor(props) {
super();
this.props = props;
}
render() {
return React.createElement(context.Consumer, null, function (context) {
var location = this.props.location || context.location;
var element, match;
React.Children.forEach(this.props.children, function (child) {
if (match == null && React.isValidElement(child)) {
element = child;
var path = child.props.path || child.props.from;
match = path ? matchPath(location.pathname, _extends({}, child.props, {
path: path
})) : context.match;
}
});
return match ? React.cloneElement(element, {
location: location,
computedMatch: match
}) : null;
});
}
}
Route
显示对应的路由节点
- 如果不是
<Switch>组件包装过的话,就需要在组件内部进行路由匹配,判断是否显示
class Route extends React.Component {
constructor(props) {
this.props = props;
}
render() {
return React.createElement(context.Consumer, null, function (context$1) {
var location = _this.props.location || context$1.location;
var match = _this.props.computedMatch ? _this.props.computedMatch // <Switch> already computed the match for us
: _this.props.path ? matchPath(location.pathname, _this.props) : context$1.match;
var props = _extends({}, context$1, {
location: location,
match: match
});
var _this$props = _this.props,
children = _this$props.children,
component = _this$props.component,
render = _this$props.render; // Preact uses an empty array as children by
// default, so use null if that's the case.
if (Array.isArray(children) && children.length === 0) {
children = null;
}
return React.createElement(context.Provider, {
value: props
}, props.match ? children ? typeof children === "function" ? process.env.NODE_ENV !== "production" ? evalChildrenDev(children, props, _this.props.path) : children(props) : children : component ? React.createElement(component, props) : render ? render(props) : null : typeof children === "function" ? process.env.NODE_ENV !== "production" ? evalChildrenDev(children, props, _this.props.path) : children(props) : null);
});
};
}
总结
简单概括就是
<BrowserRouter>内部会创建context对象,将组件的state.location作为context.Provider的value值进行传递,表示当前路由;同时也会开启popState监听事件,当监听到popState事件后,会去更新state.location的值<Link>组件会渲染自定义的component组件或者默认的a标签,并绑定对应的点击事件,pushState或replaceState,同时也会更新state.location的值<Switch>作为context的消费者组件,根据location的值筛选出与其匹配的<Route>组件来显示,当state.location的值发生改变后,<Switch>作为消费者组件就会重新渲染匹配路由的<Route>组件