改变地址栏 url 到页面更新,react router 做了什么?
React Router 实现原理是什么?
React Router 用到了 History 的哪些 API?
......
面对这些问题,脑袋里面都是 Link、Route、BrowserRouter 等等的用法那当然是不行的~
以下又是一篇学习笔记。
React Router 的基础 —— history 库
history 是一个第三方库,用来兼容在不同浏览器、不同环境下对历史记录的管理,封装了浏览器内置的 History API 和 Location API。
history 针对不同的环境提供了三个API 来创建 history 对象:
createBrowserHistory
:
- 特点:适用高版本浏览器。
- 技术:利用HTML5里面的history。
- 前进:使用
pushState
、replaceState
前进。 - 后退:监听
popstate
实现后退。
createHashHistory
:
- 特点:使用老版本浏览器。
- 技术:通过 hash 来存储在不同状态下的 history 信息。
- 前进:使用
location.hash=*** location.replace()
实现前进。 - 后退:监听
hashchange
实现后退。
createMemoryHistory
:
- 特点:适用于 node 环境下
- 技术:在内存中进行历史记录的存储。
- 前进:在内存中进行历史记录的存储。
- 后退:因为是在内存中操作,跟浏览器没有关系,不涉及UI层面的事情,所以可以直接进行历史信息的回退
另外 history 库中的 location
是 widnows.location
API的一部分,用来存储页面 url 各种信息的,比如 GET 请求中的 search, url hash,页面的位置 pathname,以及用于存储额外数据的额 state 等。比如:
{
pathname: '/here',
search: '?key=value',
hash: '#extra-information',
state: { modal: true },
key: '3wgzwe'
}
react-router-dom 的核心组件
react-router-dom
的核心就是 react-router,但多了四个组件:BrowserRouter
、 HashRouter
、Link
、NavLink
。
BrowserRouter
BrowserRouter 组件的实现很简单,就是使用 history
库中的 createBrowserHistory
接口创建了一个 history 然后传递给了 Router
组件。
import React from "react";
import { Router } from "react-router";
import { createBrowserHistory as createHistory } from "history";
/**
* The public API for a <Router> that uses HTML5 history.
*/
class [BrowserRouter](https://github.com/ReactTraining/react-router/blob/42933fe141819e4662113ab2c320bf86be3490fb/packages/react-router-dom/modules/BrowserRouter.js#L10) extends React.Component {
history = createHistory(this.props);
render() {
return <Router history={this.history} children={this.props.children} />;
}
}
Router
在 Router 组件中,调用 history 的 listen
API 监听了 location 的变化。一旦 location 发生变化,就修改 Router 中的 location。
constructor(props) {
super(props);
this.state = {
location: props.history.location
};
// This is a bit of a hack. We have to start listening for location
// changes here in the constructor in case there are any <Redirect>s
// on the initial render. If there are, they will replace/push when
// they mount and since cDM fires in children before parents, we may
// get a new location before the <Router> is mounted.
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;
}
});
}
}
Route
Route 组件中对 path 属性做了校验,其中 matchPath 函数将 path
props 跟当前 url 进行了匹配,如果满足就返回一个 match 对象,不匹配就返回 null。
// 将 path 属性和当前 URL 比对
const location = this.props.location || context.location;
const match = this.props.computedMatch
? this.props.computedMatch // <Switch> already computed the match for us
: this.props.path
? matchPath(location.pathname, this.props)
: context.match;
const props = { ...context, location, match };
let { children, component, render } = this.props;
// 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;
}
// 如果 path 匹配成功,就渲染对应的组件(包括 class 组件,function 组件以及 HTML 内容)
return (
<RouterContext.Provider value={props}>
{props.match
? children
? typeof children === "function"
? __DEV__
? evalChildrenDev(children, props, this.props.path)
: children(props)
: children
: component
? React.createElement(component, props)
: render
? render(props)
: null
: typeof children === "function"
? __DEV__
? evalChildrenDev(children, props, this.props.path)
: children(props)
: null}
</RouterContext.Provider>
);
Link
Link 组件内部定义了一个 LinkAnchor 组件,用于生成一个 <a>
标签并封装了 onClick
事件。
let props = {
...rest,
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();
}
}
};
再看看 Link 部分的逻辑。其中主要有以下几步:
- 解析 Link 的 to 属性到对应的 location。 resolveToLocation 处理了
to
参数是一个函数的情况。如果to
参数是一个字符串,就调用 history 的createLocation
API 创建一个Location
对象。 - 根据 location 生成跳转链接
- 根据 Link 的 repalce 属性判断是调用
history.replace()
还是history.push()
const { history } = context;
// 解析 to 参数(可能是 function 也可能是 string)
const location = normalizeToLocation(
resolveToLocation(to, context.location),
context.location
);
// 根据 location 生成跳转链接
const href = location ? history.createHref(location) : "";
const props = {
...rest,
href,
navigate() {
const location = resolveToLocation(to, context.location);
// 根据replace 属性判断是使用哪个 API
const method = replace ? history.replace : history.push;
method(location);
}
};
// React 15 compat
if (forwardRefShim !== forwardRef) {
props.ref = forwardedRef || innerRef;
} else {
props.innerRef = innerRef;
}
// component 是 LinkAnchor
return React.createElement(component, props);
React Router 的原理
综上,分析完核心的组件之后,大概了解了 React Router 的原理如下图所示: