原理
無刷新的更改地址欄地址 ,保證視圖和URL的同步?;驹硎荋5History 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。