原理
无刷新的更改地址栏地址 ,保证视图和URL的同步。基本原理是H5History API 。
浏览器的历史记录,以栈的形式存储,后进先出,按照栈的规律,必须有的方法:进栈(pushstate)、出栈(popstate)、替换当前的(replacestate) 。这里对H5History API就不做详细解释了,可以查看这篇文章。
实现过程
当我们点击了Link组件或者调用了
history.push
跳转路由时,react-router做了上图的处理流程。
假定使用BrowserRouter
和createBrowserHistory
,使用history.push
跳转路由
1、调用history.push
跳转路由时,内部执行window.history.pushState
在浏览器history
栈中新增一条记录,修改了应用的 URL,执行<Router></Router>
组件注册的回调函数。
2、createBrowserHistory中注册popstate事件,用户点击浏览器前进、回退时,在popstate事件中获取当前的event.state,重新组装一个location,执行<Router></Router>组件注册的回调函数。
3、history
库对外暴露createBrowserHistory
方法,react-router中实例化createBrowserHistory方法对象,在<Router>
组件中注册history.listen()回调函数,当路由有变化时,<Route>
组件中匹配location,同步UI。
分析
1、history.push
在react中,我们可以调用history.push(path,state)来跳转路由,实际执行的就是createBrowserHistory中的push方法。
在这个方法中主要做三件事:
一、根据传递的path,state参数创建一个location,不同于window.location。(具体的可以看代码,点击上面的createBrowserHistory
)
location = {
pathname, // 当前路径,即 Link 中的 to 属性
search, // search
hash, // hash
state, // state 对象
action, // location 类型,在点击 Link 时为 PUSH,浏览器前进后退时为 POP,调用 replaceState 方法时为 REPLACE
key, // 用于操作 sessionStorage 存取 state 对象
};
const location = createLocation(path, state, createKey(), history.location);
//生成location
createLocation(path, state, key, currentLocation) {
let location;
if (typeof path === 'string') {
// Two-arg form: push(path, state)
location = parsePath(path);
location.state = state;
} else {
// One-arg form: push(location)
location = { ...path };
if (location.pathname === undefined) location.pathname = '';
if (location.search) {
if (location.search.charAt(0) !== '?')
location.search = '?' + location.search;
} else {
location.search = '';
}
if (location.hash) {
if (location.hash.charAt(0) !== '#') location.hash = '#' + location.hash;
} else {
location.hash = '';
}
if (state !== undefined && location.state === undefined)
location.state = state;
}
if (key) location.key = key;
if (currentLocation) {
// Resolve incomplete/relative pathname relative to current location.
if (!location.pathname) {
location.pathname = currentLocation.pathname;
} else if (location.pathname.charAt(0) !== '/') {
location.pathname = resolvePathname(
location.pathname,
currentLocation.pathname
);
}
} else {
// When there is no prior location and pathname is empty, set it to /
if (!location.pathname) {
location.pathname = '/';
}
}
return location;
}
这个location会在<Router>
和<Route>
组件中使用,来根据location中的值和<Route path='xxx'></Route>
中的path匹配,匹配成功的Route组件渲染指定的component;
二、执行globalHistory.pushState({ key, state }, null, href)
,添加一条记录;并执行 setState({ action, location })
;
三、在setState({ action, location })
中执行Router中注册的listener,transitionManager.notifyListeners(history.location, history.action)
。
function setState(nextState) {
Object.assign(history, nextState);
history.length = globalHistory.length;
transitionManager.notifyListeners(history.location, history.action);
}
function notifyListeners(...args) {
listeners.forEach(listener => listener(...args));
}
2、popstate
事件
popstate事件触发时,可以得到event.state
,createBrowserHistory中会根据这个state和当前window.location重新生成一个location对象,执行Router组件注册的listener,同步UI。
3、<Router>
组件
BrowserRouter组件中会实例化一个createBrowserHistory对象,传递给Router组件
import React from "react";
import { Router } from "react-router";
import { createBrowserHistory as createHistory } from "history";
import PropTypes from "prop-types";
import warning from "tiny-warning";
/**
* The public API for a <Router> that uses HTML5 history.
*/
class BrowserRouter extends React.Component {
history = createHistory(this.props);
render() {
return <Router history={this.history} children={this.props.children} />;
}
}
在Router
组件中要注册history.listen()的一个监听函数,并且保存一份子组件(Route)使用的数据
Router.prototype.componentWillMount = function componentWillMount() {
var _this2 = this;
var _props = this.props,
children = _props.children,
history = _props.history;
(0, _invariant2.default)(children == null || _react2.default.Children.count(children) === 1, "A <Router> may have only one child element");
// Do this here so we can setState when a <Redirect> changes the
// location in componentWillMount. This happens e.g. when doing
// server rendering using a <StaticRouter>.
this.unlisten = history.listen(function () {
_this2.setState({
match: _this2.computeMatch(history.location.pathname)
});
});
};
Router.prototype.getChildContext = function getChildContext() {
return {
router: _extends({}, this.context.router, {
history: this.props.history,
route: {
location: this.props.history.location,
match: this.state.match
}
})
};
};
当调用history.push
或触发popstate
事件时,这里注册的listener都会被createBrowserHistory
执行,触发setState,然后Router的子组件中匹配的<Route>会重新渲染.
4、<Route>
组件
在Route中有一个match状态,在父组件props发生变化时会重新计算
Route.prototype.componentWillReceiveProps = function componentWillReceiveProps(nextProps, nextContext) {
(0, _warning2.default)(!(nextProps.location && !this.props.location), '<Route> elements should not change from uncontrolled to controlled (or vice versa). You initially used no "location" prop and then provided one on a subsequent render.');
(0, _warning2.default)(!(!nextProps.location && this.props.location), '<Route> elements should not change from controlled to uncontrolled (or vice versa). You provided a "location" prop initially but omitted it on a subsequent render.');
this.setState({
match: this.computeMatch(nextProps, nextContext.router)
});
};
//computeMatch主要工作就是匹配当前组件上指定的path和当前浏览器的路径是否一致,一致就渲染组件
Route.prototype.computeMatch = function computeMatch(_ref, router) {
var computedMatch = _ref.computedMatch,
location = _ref.location,
path = _ref.path,
strict = _ref.strict,
exact = _ref.exact,
sensitive = _ref.sensitive;
if (computedMatch) return computedMatch; // <Switch> already computed the match for us
(0, _invariant2.default)(router, "You should not use <Route> or withRouter() outside a <Router>");
var route = router.route;
var pathname = (location || route.location).pathname;
return (0, _matchPath2.default)(pathname, { path: path, strict: strict, exact: exact, sensitive: sensitive }, route.match);
};
总结
总结一下,react-router的路由机制就是:
1、借助history库,history中实现了push、go、goBack等方法,注册了popstate事件,当路由跳转时,使用浏览器内置的history api 操作 history栈。
2、history库对外暴露的history对象提供了listen方法,<Router></Router>
组件会注册一个listener;
3、当调用hsitory.push或popstate事件触发时,执行listener。
4、<Router></Router>
注册的监听函数内部会setState更新状态
5、<Router></Router>
的子组件<Route>
的componentWillReceiveProps生命周期函数中能得到Router中context,根据当前path和浏览器当前location来判断当前route是否match,匹配就渲染component。