git clone https://github.com/ReactTraining/history.git
“version”: “4.10.1”
/modules/index.js
export { default as createBrowserHistory } from './createBrowserHistory';export { default as createHashHistory } from './createHashHistory';export { default as createMemoryHistory } from './createMemoryHistory';export { createLocation, locationsAreEqual } from './LocationUtils';export { parsePath, createPath } from './PathUtils';
createLocation
import resolvePathname from 'resolve-pathname';import valueEqual from 'value-equal';// import { parsePath } from './PathUtils.js';function parsePath(path) {let pathname = path || '/';let search = '';let hash = '';const hashIndex = pathname.indexOf('#');if (hashIndex !== -1) {hash = pathname.substr(hashIndex);pathname = pathname.substr(0, hashIndex);}const searchIndex = pathname.indexOf('?');if (searchIndex !== -1) {search = pathname.substr(searchIndex);pathname = pathname.substr(0, searchIndex);}return {pathname,search: search === '?' ? '' : search,hash: hash === '#' ? '' : hash};}export function 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;}try {location.pathname = decodeURI(location.pathname);} catch (e) {if (e instanceof URIError) {throw new URIError('Pathname "' +location.pathname +'" could not be decoded. ' +'This is likely caused by an invalid percent-encoding.');} else {throw e;}}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;}export function locationsAreEqual(a, b) {return (a.pathname === b.pathname &&a.search === b.search &&a.hash === b.hash &&a.key === b.key &&valueEqual(a.state, b.state));}
createBrowserHistory
import { createLocation } from './LocationUtils.js';import {addLeadingSlash,stripTrailingSlash,hasBasename,stripBasename,createPath} from './PathUtils.js';import createTransitionManager from './createTransitionManager.js';import {canUseDOM,getConfirmation,supportsPopStateOnHashChange,isExtraneousPopstateEvent} from './DOMUtils.js';import invariant from './invariant.js';import warning from './warning.js';const PopStateEvent = 'popstate';const HashChangeEvent = 'hashchange';function getHistoryState() {try {return window.history.state || {};} catch (e) {// IE 11 sometimes throws when accessing window.history.state// See https://github.com/ReactTraining/history/pull/289return {};}}function supportsHistory() {const ua = window.navigator.userAgent;if ((ua.indexOf('Android 2.') !== -1 || ua.indexOf('Android 4.0') !== -1) &&ua.indexOf('Mobile Safari') !== -1 &&ua.indexOf('Chrome') === -1 &&ua.indexOf('Windows Phone') === -1)return false;return window.history && 'pushState' in window.history;}/*** Creates a history object that uses the HTML5 history API including* pushState, replaceState, and the popstate event.*/function createBrowserHistory(props = {}) {invariant(canUseDOM, 'Browser history needs a DOM');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)): '';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 interfacefunction 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) {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;}export default createBrowserHistory;
createTransitionManager 监听器
当BrowserHistory发出setState时,会执行对应的listener
import warning from './warning.js';function createTransitionManager() {let prompt = null;function setPrompt(nextPrompt) {warning(prompt == null, 'A history supports only one prompt at a time');prompt = nextPrompt;return () => {if (prompt === nextPrompt) prompt = null;};}function confirmTransitionTo(location,action,getUserConfirmation,callback) {// TODO: If another transition starts while we're still confirming// the previous one, we may end up in a weird state. Figure out the// best way to handle this.if (prompt != null) {const result =typeof prompt === 'function' ? prompt(location, action) : prompt;if (typeof result === 'string') {if (typeof getUserConfirmation === 'function') {getUserConfirmation(result, callback);} else {warning(false,'A history needs a getUserConfirmation function in order to use a prompt message');callback(true);}} else {// Return false from a transition hook to cancel the transition.callback(result !== false);}} else {callback(true);}}let listeners = [];function appendListener(fn) {let isActive = true;function listener(...args) {if (isActive) fn(...args);}listeners.push(listener);return () => {isActive = false;listeners = listeners.filter(item => item !== listener);};}function notifyListeners(...args) {listeners.forEach(listener => listener(...args));}return {setPrompt,confirmTransitionTo,appendListener,notifyListeners};}export default createTransitionManager;
