原理
無(wú)刷新的更改地址欄地址 ,保證視圖和URL的同步?;驹硎荋5History API 。
瀏覽器的歷史記錄,以棧的形式存儲(chǔ),后進(jìn)先出,按照棧的規(guī)律,必須有的方法:進(jìn)棧(pushstate)、出棧(popstate)、替換當(dāng)前的(replacestate) 。這里對(duì)H5History API就不做詳細(xì)解釋了,可以查看這篇文章。
實(shí)現(xiàn)過(guò)程
當(dāng)我們點(diǎn)擊了Link組件或者調(diào)用了
history.push
跳轉(zhuǎn)路由時(shí),react-router做了上圖的處理流程。
假定使用BrowserRouter
和createBrowserHistory
,使用history.push
跳轉(zhuǎn)路由
1、調(diào)用history.push
跳轉(zhuǎn)路由時(shí),內(nèi)部執(zhí)行window.history.pushState
在瀏覽器history
棧中新增一條記錄,修改了應(yīng)用的 URL,執(zhí)行<Router></Router>
組件注冊(cè)的回調(diào)函數(shù)。
2、createBrowserHistory中注冊(cè)popstate事件,用戶點(diǎn)擊瀏覽器前進(jìn)、回退時(shí),在popstate事件中獲取當(dāng)前的event.state,重新組裝一個(gè)location,執(zhí)行<Router></Router>組件注冊(cè)的回調(diào)函數(shù)。
3、history
庫(kù)對(duì)外暴露createBrowserHistory
方法,react-router中實(shí)例化createBrowserHistory方法對(duì)象,在<Router>
組件中注冊(cè)history.listen()回調(diào)函數(shù),當(dāng)路由有變化時(shí),<Route>
組件中匹配location,同步UI。
分析
1、history.push
在react中,我們可以調(diào)用history.push(path,state)來(lái)跳轉(zhuǎn)路由,實(shí)際執(zhí)行的就是createBrowserHistory中的push方法。
在這個(gè)方法中主要做三件事:
一、根據(jù)傳遞的path,state參數(shù)創(chuàng)建一個(gè)location,不同于window.location。(具體的可以看代碼,點(diǎn)擊上面的createBrowserHistory
)
location = {
pathname, // 當(dāng)前路徑,即 Link 中的 to 屬性
search, // search
hash, // hash
state, // state 對(duì)象
action, // location 類型,在點(diǎn)擊 Link 時(shí)為 PUSH,瀏覽器前進(jìn)后退時(shí)為 POP,調(diào)用 replaceState 方法時(shí)為 REPLACE
key, // 用于操作 sessionStorage 存取 state 對(duì)象
};
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;
}
這個(gè)location會(huì)在<Router>
和<Route>
組件中使用,來(lái)根據(jù)location中的值和<Route path='xxx'></Route>
中的path匹配,匹配成功的Route組件渲染指定的component;
二、執(zhí)行globalHistory.pushState({ key, state }, null, href)
,添加一條記錄;并執(zhí)行 setState({ action, location })
;
三、在setState({ action, location })
中執(zhí)行Router中注冊(cè)的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事件觸發(fā)時(shí),可以得到event.state
,createBrowserHistory中會(huì)根據(jù)這個(gè)state和當(dāng)前window.location重新生成一個(gè)location對(duì)象,執(zhí)行Router組件注冊(cè)的listener,同步UI。
3、<Router>
組件
BrowserRouter組件中會(huì)實(shí)例化一個(gè)createBrowserHistory對(duì)象,傳遞給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
組件中要注冊(cè)history.listen()的一個(gè)監(jiān)聽(tīng)函數(shù),并且保存一份子組件(Route)使用的數(shù)據(jù)
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
}
})
};
};
當(dāng)調(diào)用history.push
或觸發(fā)popstate
事件時(shí),這里注冊(cè)的listener都會(huì)被createBrowserHistory
執(zhí)行,觸發(fā)setState,然后Router的子組件中匹配的<Route>會(huì)重新渲染.
4、<Route>
組件
在Route中有一個(gè)match狀態(tài),在父組件props發(fā)生變化時(shí)會(huì)重新計(jì)算
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主要工作就是匹配當(dāng)前組件上指定的path和當(dāng)前瀏覽器的路徑是否一致,一致就渲染組件
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);
};
總結(jié)
總結(jié)一下,react-router的路由機(jī)制就是:
1、借助history庫(kù),history中實(shí)現(xiàn)了push、go、goBack等方法,注冊(cè)了popstate事件,當(dāng)路由跳轉(zhuǎn)時(shí),使用瀏覽器內(nèi)置的history api 操作 history棧。
2、history庫(kù)對(duì)外暴露的history對(duì)象提供了listen方法,<Router></Router>
組件會(huì)注冊(cè)一個(gè)listener;
3、當(dāng)調(diào)用hsitory.push或popstate事件觸發(fā)時(shí),執(zhí)行l(wèi)istener。
4、<Router></Router>
注冊(cè)的監(jiān)聽(tīng)函數(shù)內(nèi)部會(huì)setState更新?tīng)顟B(tài)
5、<Router></Router>
的子組件<Route>
的componentWillReceiveProps生命周期函數(shù)中能得到Router中context,根據(jù)當(dāng)前path和瀏覽器當(dāng)前l(fā)ocation來(lái)判斷當(dāng)前route是否match,匹配就渲染component。