createBrowserHistory.js
function createBrowserHistory(props = {}) {
invariant(canUseDOM, "Browser history needs a DOM");
// 获得全局的history
const globalHistory = window.history;
const canUseHistory = supportsHistory();
const needsHashChangeListener = !supportsPopStateOnHashChange();
const {
forceRefresh = false,
getUserConfirmation = getConfirmation,
keyLength = 6
} = props;
const basename = props.basename
? stripTrailingSlash(addLeadingSlash(props.basename))
: "";
// 获取location对象
function getDOMLocation(historyState) {
const { key, state } = historyState || {};
const { pathname, search, hash } = window.location;
let path = pathname + search + hash;
warning(
!basename || hasBasename(path, basename),
"You are attempting to use a basename on a page whose URL path does not begin " +
'with the basename. Expected path "' +
path +
'" to begin with "' +
basename +
'".'
);
if (basename) path = stripBasename(path, basename);
return createLocation(path, state, key);
}
function createKey() {
return Math.random()
.toString(36)
.substr(2, keyLength);
}
const transitionManager = createTransitionManager();
function setState(nextState) {
Object.assign(history, nextState);
history.length = globalHistory.length;
transitionManager.notifyListeners(history.location, history.action);
}
function handlePopState(event) {
// Ignore extraneous popstate events in WebKit.
if (isExtraneousPopstateEvent(event)) return;
handlePop(getDOMLocation(event.state));
}
function handleHashChange() {
handlePop(getDOMLocation(getHistoryState()));
}
let forceNextPop = false;
function handlePop(location) {
if (forceNextPop) {
forceNextPop = false;
setState();
} else {
const action = "POP";
transitionManager.confirmTransitionTo(
location,
action,
getUserConfirmation,
ok => {
if (ok) {
setState({ action, location });
} else {
revertPop(location);
}
}
);
}
}
function revertPop(fromLocation) {
const toLocation = history.location;
// TODO: We could probably make this more reliable by
// keeping a list of keys we've seen in sessionStorage.
// Instead, we just default to 0 for keys we don't know.
let toIndex = allKeys.indexOf(toLocation.key);
if (toIndex === -1) toIndex = 0;
let fromIndex = allKeys.indexOf(fromLocation.key);
if (fromIndex === -1) fromIndex = 0;
const delta = toIndex - fromIndex;
if (delta) {
forceNextPop = true;
go(delta);
}
}
const initialLocation = getDOMLocation(getHistoryState());
let allKeys = [initialLocation.key];
// Public interface
function createHref(location) {
return basename + createPath(location);
}
function push(path, state) {
warning(
!(
typeof path === "object" &&
path.state !== undefined &&
state !== undefined
),
"You should avoid providing a 2nd state argument to push when the 1st " +
"argument is a location-like object that already has state; it is ignored"
);
const action = "PUSH";
const location = createLocation(
path,
state,
createKey(),
history.location
);
transitionManager.confirmTransitionTo(
location,
action,
getUserConfirmation,
ok => {
if (!ok) return;
const href = createHref(location);
const { key, state } = location;
if (canUseHistory) {
globalHistory.pushState({ key, state }, null, href);
if (forceRefresh) {
window.location.href = href;
} else {
const prevIndex = allKeys.indexOf(history.location.key);
const nextKeys = allKeys.slice(0, prevIndex + 1);
nextKeys.push(location.key);
allKeys = nextKeys;
setState({ action, location });
}
} else {
warning(
state === undefined,
"Browser history cannot push state in browsers that do not support HTML5 history"
);
window.location.href = href;
}
}
);
}
function replace(path, state) {
warning(
!(
typeof path === "object" &&
path.state !== undefined &&
state !== undefined
),
"You should avoid providing a 2nd state argument to replace when the 1st " +
"argument is a location-like object that already has state; it is ignored"
);
const action = "REPLACE";
const location = createLocation(
path,
state,
createKey(),
history.location
);
transitionManager.confirmTransitionTo(
location,
action,
getUserConfirmation,
ok => {
if (!ok) return;
const href = createHref(location);
const { key, state } = location;
if (canUseHistory) {
globalHistory.replaceState({ key, state }, null, href);
if (forceRefresh) {
window.location.replace(href);
} else {
const prevIndex = allKeys.indexOf(history.location.key);
if (prevIndex !== -1) allKeys[prevIndex] = location.key;
setState({ action, location });
}
} else {
warning(
state === undefined,
"Browser history cannot replace state in browsers that do not support HTML5 history"
);
window.location.replace(href);
}
}
);
}
function go(n) {
globalHistory.go(n);
}
function goBack() {
go(-1);
}
function goForward() {
go(1);
}
let listenerCount = 0;
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);
}
}
let isBlocked = false;
function block(prompt = false) {
const unblock = transitionManager.setPrompt(prompt);
if (!isBlocked) {
checkDOMListeners(1);
isBlocked = true;
}
return () => {
if (isBlocked) {
isBlocked = false;
checkDOMListeners(-1);
}
return unblock();
};
}
function listen(listener) {
// 监听location的变化,并且返回一个取消监听的函数
const unlisten = transitionManager.appendListener(listener);
checkDOMListeners(1);
return () => {
checkDOMListeners(-1);
unlisten();
};
}
const history = {
length: globalHistory.length,
action: "POP",
location: initialLocation,
createHref,
push,
replace,
go,
goBack,
goForward,
block,
listen
};
return history;
}
返回一个 history 对象,包含一系列用来操控浏览器会话历史的方法,