一 核心api
1 Router-接收location变化,派发更新流
Router
作用是把 history location
等路由信息 传递下去
Router
/* Router 作用是把 history location 等路由信息 传递下去 */
class Router extends React.Component {
static computeRootMatch(pathname) {
return {
path: '/', url: '/', params: {
}, isExact: pathname === '/' };
}
constructor(props) {
super(props);
this.state = {
location: props.history.location
};
//记录pending位置
//如果存在任何<Redirect>,则在构造函数中进行更改
//在初始渲染时。如果有,它们将在
//在子组件身上激活,我们可能会
//在安装<Router>之前获取一个新位置。
this._isMounted = false;
this._pendingLocation = null;
/* 此时的history,是history创建的history对象 */
if (!props.staticContext) {
/* 这里判断 componentDidMount 和 history.listen 执行顺序 然后把 location复制 ,防止组件重新渲染 */
this.unlisten = props.history.listen(location => {
/* 创建监听者 */
if (this._isMounted) {
this.setState({
location });
} else {
this._pendingLocation = location;
}
});
}
}
componentDidMount() {
this._isMounted = true;
if (this._pendingLocation) {
this.setState({
location: this._pendingLocation });
}
}
componentWillUnmount() {
/* 解除监听 */
if (this.unlisten) this.unlisten();
}
render() {
return (
/* 这里可以理解 react.createContext 创建一个 context上下文 ,保存router基本信息。children */
<RouterContext.Provider
children={
this.props.children || null}
value={
{
history: this.props.history,
location: this.state.location,
match: Router.computeRootMatch(this.state.location.pathname),
staticContext: this.props.staticContext
}}
/>
);
}
}
总结:
初始化绑定listen, 路由变化,通知改变location
,改变组件。 react的history路由状态是保存在React.Content
上下文之间, 状态更新。
一个项目应该有一个根Router
, 来产生切换路由组件之前的更新作用。
如果存在多个Router
会造成,会造成切换路由,页面不更新的情况。
2 Switch-匹配正确的唯一的路由
根据router更新流,来渲染当前组件。
/* switch组件 */
class Switch extends React.Component {
render() {
return (
<RouterContext.Consumer>
{
/* 含有 history location 对象的 context */}
{
context => {
invariant(context, 'You should not use <Switch> outside a <Router>');
const location = this.props.location || context.location;
let element, match;
//我们使用React.Children.forEach而不是React.Children.toArray().find()
//这里是因为toArray向所有子元素添加了键,我们不希望
//为呈现相同的两个<Route>s触发卸载/重新装载
//组件位于不同的URL。
//这里只需然第一个 含有 match === null 的组件
React.Children.forEach(this.props.children, child => {
if (match == null && React.isValidElement(child)) {
element = child;
// 子组件 也就是 获取 Route中的 path 或者 rediect 的 from
const path = child.props.path || child.props.from;
match = path
? matchPath(location.pathname, {
...child.props, path })
: context.match;
}
});
return match
? React.cloneElement(element, {
location, computedMatch: match })
: null;
}}
</RouterContext.Consumer>
);
}
}
找到与当前path,匹配的组件进行渲染。 通过pathname
和组件的path
进行匹配。找到符合path的router组件。
matchPath
function matchPath(pathname, options = {
}) {
if (typeof options === "string" || Array.isArray(options)) {
options = {
path: options };
}
const {
path, exact = false, strict = false, sensitive = false } = options;
const paths = [].concat(path);
return paths.reduce((matched, path) => {
if (!path && path !== "") return null;
if (matched) return matched;
const {
regexp, keys } = compilePath(path, {
end: exact,
strict,
sensitive
});
const match = regexp.exec(pathname);
/* 匹配不成功,返回null */
if (!match) return null;
const [url, ...values] = match;
const isExact = pathname === url;
if (exact && !isExact) return null;
return {
path, // the path 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;
}, {
})
};
}, null);
}
匹配符合的路由。
3 Route-组件页面承载容器
/**
* The public API for matching a single path and rendering.
*/
class Route extends React.Component {
render() {
return (
<RouterContext.Consumer>
{
context => {
/* router / route 会给予警告警告 */
invariant(context, "You should not use <Route> outside a <Router>");
// computedMatch 为 经过 swich处理后的 path
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;
if (Array.isArray(children) && children.length === 0) {
children = null;
}
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>
);
}}
</RouterContext.Consumer>
);
}
}
匹配path,渲染组件。作为路由组件的容器,可以根据将实际的组件渲染出来。通过RouterContext.Consume
取出当前上一级的location,match
等信息。作为prop传递给页面组件。使得我们可以在页面组件中的props中获取location ,match
等信息。
4 Redirect-没有符合的路由,那么重定向
重定向组件, 如果来路由匹配上,会重定向对应的路由。
function Redirect({
computedMatch, to, push = false }) {
return (
<RouterContext.Consumer>
{
context => {
const {
history, staticContext } = context;
/* method就是路由跳转方法。 */
const method = push ? history.push : history.replace;
/* 找到符合match的location ,格式化location */
const location = createLocation(
computedMatch
? typeof to === 'string'
? generatePath(to, computedMatch.params)
: {
...to,
pathname: generatePath(to.pathname, computedMatch.params)
}
: to
)
/* 初始化的时候进行路由跳转,当初始化的时候,mounted执行push方法,当组件更新的时候,如果location不相等。同样会执行history方法重定向 */
return (
<Lifecycle
onMount={
() => {
method(location);
}}
onUpdate={
(self, prevProps) => {
const prevLocation = createLocation(prevProps.to);
if (
!locationsAreEqual(prevLocation, {
...location,
key: prevLocation.key
})
) {
method(location);
}
}}
to={
to}
/>
);
}}
</RouterContext.Consumer>
);
}
初始化的时候进行路由跳转,当初始化的时候,mounted
执行push
方法,当组件更新的时候,如果location
不相等。同样会执行history
方法重定向。
二 总结 + 流程分析
总结
history
提供了核心api,如监听路由,更改路由的方法,已经保存路由状态state。
react-router
提供路由渲染组件,路由唯一性匹配组件,重定向组件等功能组件。
流程分析
当地址栏改变url,组件的更新渲染都经历了什么?😊😊😊
拿history模式做参考。当url改变,首先触发histoy,调用事件监听popstate
事件, 触发回调函数handlePopState
,触发history下面的setstate
方法,产生新的location对象,然后通知Router组件更新location
并通过context上下文传递,
switch
通过传递的更新流,匹配出符合的Route组件渲染,最后有Route
组件取出context
内容,传递给渲染页面,渲染更新。
当我们调用history.push
方法,切换路由,组件的更新渲染又都经历了什么呢?
我们还是拿history模式作为参考,当我们调用history.push
方法,首先调用history的push
方法,通过history.pushState
来改变当前url
,接下来触发history下面的setState
方法,接下来的步骤就和上面一模一样了,这里就不一一说了。
我们用一幅图来表示各个路由组件之间的关系。
希望读过此篇文章的朋友,能够明白react-router的整个流程,代码逻辑不是很难理解。整个流程我给大家分析了一遍,希望同学们能主动看一波源码,把整个流程搞明白。纸上得来终觉浅,绝知此事要躬行。
写在最后,谢谢大家鼓励与支持🌹🌹🌹,喜欢的可以给笔者点赞关注,公众号:前端Sharing