這個庫輸出的模塊有:node
exports.createBrowserHistory = createBrowserHistory;
exports.createHashHistory = createHashHistory;
exports.createMemoryHistory = createMemoryHistory;
exports.createLocation = createLocation;
exports.locationsAreEqual = locationsAreEqual;
exports.parsePath = parsePath;
exports.createPath = createPath;複製代碼
這裏重點分析 createBrowserHistory createMemoryHistoryreact
首先看createBrowserHistory的返回是:web
var history = {
length: globalHistory.length,
action: 'POP',
location: initialLocation,
createHref: createHref,
push: push,
replace: replace,
go: go,
goBack: goBack,
goForward: goForward,
block: block,
listen: listen
};
return history;複製代碼
createBrowserHistory主要是基於h5 的history中的pushstate以及replacestate來變動瀏覽器地址欄。當瀏覽器不支持h5時,會用location.href直接賦值的辦法直接跳轉頁面,檢測是否支持h5 的函數supportsHistory,以及 needsHashChangeListener 是判斷是否支持hashchange檢測的,以下代碼:npm
/**
* Returns true if browser fires popstate on hash change.
* IE10 and IE11 do not.
*/
function supportsPopStateOnHashChange() {
return window.navigator.userAgent.indexOf('Trident') === -1;
}
var needsHashChangeListener = !supportsPopStateOnHashChange();複製代碼
createBrowserHistory 的History其實使用的就是window.history的api,會利用window.addEventListener監聽popstate和hashchange事件,而且會將listener的回調函數加到隊列中,react-router的回調函數是:api
function (location) {
if (_this._isMounted) {
_this.setState({
location: location
});
} else {
_this._pendingLocation = location;
}
}複製代碼
其中location就是邏輯圖最後listener.apply的參數args,即history 主要是維護一個history棧,監聽瀏覽器變化,控制history的棧記錄,而且返回當前location信息給react-router,react-router會根據相應的location render出對應的components。數組
function checkDOMListeners(delta) {
listenerCount += delta;
if (listenerCount === 1 && delta === 1) {
window.addEventListener(PopStateEvent, handlePopState);
if (needsHashChangeListener) window.addEventListener(HashChangeEvent, handleHashChange);
} else if (listenerCount === 0) {
window.removeEventListener(PopStateEvent, handlePopState);
if (needsHashChangeListener) window.removeEventListener(HashChangeEvent, handleHashChange);
}
}複製代碼
如上,listenerCount是現有的監聽事件函數,checkDOMListeners只在listen函數以及block函數中被調用,參數只爲1 與 -1.因此以此來控制add監聽或remove監聽。瀏覽器
handlePop函數是在popstate事件觸發時的回調函數,主要就是setState action以及location。而setState方法首先利用_extends方法把action和history覆蓋到history同屬性值上,也就值替換掉當前的state和location。緩存
其次調用notifyListeners方法,這個方法就是調用在listeners隊列中的listener.apply()回調方法也就是上面提到的react-router中的listen方法裏的函數,而history裏的location會傳給react-router裏的listen回調函數來使用。bash
react-router拿到了當前應該展示那個location頁面組件,接下來就是react-router的舞臺了。react-router
function handlePop(location) {
if (forceNextPop) {
forceNextPop = false;
setState();
} else {
var action = 'POP';
transitionManager.confirmTransitionTo(location, action, getUserConfirmation, function (ok) {
if (ok) {
setState({
action: action,
location: location
});
} else {
revertPop(location);
}
});
}
}複製代碼
//setState代碼
function setState(nextState) {
_extends(history, nextState);
history.length = globalHistory.length;
transitionManager.notifyListeners(history.location, history.action);
}複製代碼
//notifyListeners代碼
function notifyListeners() {
for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
args[_key] = arguments[_key];
}
listeners.forEach(function (listener) {
return listener.apply(void 0, args);
});
}複製代碼
以上方法其實都是調用的window.history的原生api,以下
function push(path, state) {
var location = createLocation(path, state, createKey(), history.location);
transitionManager.confirmTransitionTo(location, action, getUserConfirmation, function (ok) {
if (!ok) return;
var href = createHref(location);
var key = location.key,
state = location.state;
if (canUseHistory) {
globalHistory.pushState({
key: key,
state: state
}, null, href);
if (forceRefresh) {
window.location.href = href;
} else {
//重點代碼:
/**************************/
// 更新存儲的allKeys
// allKeys 緩存歷史堆棧中的數據標識
// 當location處於history隊尾時,實際爲push
// 當location處於history中間時,會刪除以後的keys,並添加新key ??????????TODO:::::::::
var prevIndex = allKeys.indexOf(history.location.key);
var nextKeys = allKeys.slice(0, prevIndex === -1 ? 0 : prevIndex + 1);
nextKeys.push(location.key);
allKeys = nextKeys;
setState({
action: action,
location: location
});
/*****************************/
}
} else {
warning(state === undefined, 'Browser history cannot push state in browsers that do not support HTML5 history');
window.location.href = href;
}
});
}複製代碼
//replace方法點睛之筆:
var prevIndex = allKeys.indexOf(history.location.key);
if (prevIndex !== -1) allKeys[prevIndex] = location.key;
setState({
action: action,
location: location
});複製代碼
function go(n) {
globalHistory.go(n);
}
function goBack() {
go(-1);
}
function goForward() {
go(1);
}複製代碼
createMemoryHistory的邏輯同createBrowserHistory,可是不是直接使用的window.history的api。
createMemoryHistory 用於在內存中建立徹底虛擬的歷史堆棧,只緩存歷史記錄,但與真實的地址欄無關(不會引發地址欄變動,不會和原生的 history 對象保持同步),也與 popstate, hashchange 事件無關。
createMemoryHistory 的參數 props 接受 getUserConfirmation, initialEntries, initialIndex, keyLength 屬性。其中,props.initialEntries 指定最初的歷史堆棧內容 history.entries;props.initialIndex 指定最初的索引值 history.index。push, replace 方法均將改變 history.entries 歷史堆棧內容;go, goBack, goForward 均基於 history.entries 歷史堆棧內容,以改變 history.index 及 history.location。實現參見源碼。
這個庫輸出的模塊有:
exports.MemoryRouter = MemoryRouter;
exports.Prompt = Prompt;
exports.Redirect = Redirect;
exports.Route = Route;
exports.Router = Router;
exports.StaticRouter = StaticRouter;
exports.Switch = Switch;
exports.generatePath = generatePath;
exports.matchPath = matchPath;
exports.withRouter = withRouter;
exports.__RouterContext = context;複製代碼
重點解析route router
Router裏會傳入history,history通常是利用history npm包實例化的實例。
var context =
/*#__PURE__*/
createNamedContext("Router");//(1)
var Router =
function (_React$Componet) {
_inheritsLoose(Router,_React$Component);//(2)
Router.computeRootMatch = function computeRootMatch(pathname) { //(3)
return {
path: "/",
url: "/",
params: {},
isExact: pathname === "/"
};
};
function Router(props) {
、、、
、、、
_this._isMounted = false;
_this._pendingLocation = null;
if (!props.staticContext) {
_this.unlisten = props.history.listen(function (location) { //(4) 這裏location是props的location
if (_this._isMounted) {
_this.setState({
location: location
});
} else {
_this._pendingLocation = location;
}
});
}
return _this;
}
var _proto = Router.prototype;
_proto.componentDidMount = function componentDidMount() {//(6)
this._isMounted = true;
if (this._pendingLocation) {
this.setState({
location: this._pendingLocation
});
}
};
_proto.componentWillUnmount = function componentWillUnmount() {//(7)
if (this.unlisten) this.unlisten();
};
_proto.render = function render() { //(5)
return React.createElement(context.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
}
});
};
return Router;
}(React.Component);
{
Router.propTypes = {
children: PropTypes.node,
history: PropTypes.object.isRequired,
staticContext: PropTypes.object
};
Router.prototype.componentDidUpdate = function (prevProps) {
warning(prevProps.history === this.props.history, "You cannot change <Router history>");
};
}複製代碼
(1)createNamedContext會調用createContext方法,createContext是require的mini-create-react-context,代碼大致以下:
//provider
var Provider = {
function Provider() {
var _this;
_this = _Component.apply(this, arguments) || this;
_this.emitter = createEventEmitter(_this.props.value);//createEventEmitter定義了handler=[],實現了on(handler)、off(handler)、get(handler)、set(handler)方法來不一樣的將函數放到handler數組後刪除;
return _this;
}
var _proto = Provider.prototype;
_proto.getChildContext = function getChildContext() { //getChildContext方法:向react context中綁定全局變量。
var _ref;
return _ref = {}, _ref[contextProp] = this.emitter, _ref;
};
_proto.componentWillReceiveProps = function componentWillReceiveProps(nextProps) {
if (this.props.value !== nextProps.value) {
var oldValue = this.props.value;
var newValue = nextProps.value;
、、、
this.emitter.set(nextProps.value, changedBits);//當createContext有第二個參數時才起做用,因此這裏暫時留個疑問,沒有立刻看懂。TODO::::::::::::::
、、、
}
}
}
//consumer
`````
````複製代碼
(2) _inheritsLoose(Router,_React$Component);是Router繼承_React$Component,而_React$Component就是Router包裹的組件。
(3)computeRootMatch Router會有一個默認值
(4)Router會有一個history.listen也就是講history時的listen函數,當前location也就是經過這裏傳到了react-router的。
可是這裏沒有很明白一點:在listen這部分有一塊註釋,意思是當組件mount以前,若是發生了popstate、hashchange事件,那我能夠及時的在mount以前就利用_this._pendingLocation = location;那樣在componentDidmount的時候就能夠用最新的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.複製代碼
(5)Router render:
其實就是將children組件render出來,不過react16里加了provider,因此這裏會將以前context.provider裏的屬性對象等也會包裹進來。
(6)componentDidMount 結合上滿(4)
(7)componentWillUnmount 組件卸載時,會調用this.unlisten解除Router監聽事件。
從app.js 中,發現 Route 使用方式是<Route exact path="/" component={Home}/>
var Route = function(_React$component) {
function Route() {
return _React$Component.apply(this, arguments) || this;
}
var _proto = Route.prototype;
_proto.render = function render() {
return React.createElement(context.Consumer, null, function (context$$1) {
var location = _this.props.location || context$$1.location;
var match = _this.props.computedMatch ? _this.props.computedMatch // <Switch> already computed the match for us
: _this.props.path ? matchPath(location.pathname, _this.props) : context$$1.match;
var props = _extends({}, context$$1, {
location: location,
match: match
});
return React.createElement(context.Provider, {
value: props
}, children && !isEmptyChildren(children) ? children : props.match ? component ? React.createElement(component, props) : render ? render(props) : null : null);
});
}
}
}複製代碼
從render 方法能夠知道,會經過match、location、path來匹配是否和當前location符合,而後決定render到哪一個具體的children組件。
props = {match, location, history, staticContext} 這些屬性在組件中會有很大的用途
1 history:zhuanlan.zhihu.com/p/55837818
2 react-router:juejin.im/post/5b8251…
3 react-router4官方文檔:reacttraining.com/react-route…