本篇文档解读的是history.js4.10.1版本,虽然现在已经升级到了5.0版本(react-router6版本使用的是history5版本),但是我还是打算两个版本都了解一下,阅读源码之前充分详细的阅读它的官方文档是很重要的,
首先是目录,可以看到大致和官方文档相符,三个关键的api:createBrowserHistory,createHashHistory,createMemoryHistory,以及其他一下工具文件,
点开index.js还是老样子暴露出去了几个api,我们从最常用的createBrowserHistory看起
createBrowserHistory.js
可以看到直接暴露出去了一个function,这个function返回了一个history对象,包含了函数内声明的一些方法(看名字应该就能大致知道作用)
根据在使用时候的用法首先会调用createBrowserHistory,我们先看一下在初始化的时候会做些什么
初始化
invariant(canUseDOM, 'Browser history needs a DOM');
// 全局history
const globalHistory = window.history;
// 是否可以使用history pushState api
const canUseHistory = supportsHistory();
// 是否需要hashChangeListener
const needsHashChangeListener = !supportsPopStateOnHashChange();
// 创建管理对象
const transitionManager = createTransitionManager();
// 初始化location
const initialLocation = getDOMLocation(getHistoryState());
// 所有的key
let allKeys = [initialLocation.key];
首先使用了canUseDom方法和tiny-invariant这个库,来判断当前环境,如果不是浏览器环境则会抛出错误
canUseDom
export const canUseDOM = !!(
typeof window !== 'undefined' &&
window.document &&
window.document.createElement
);
返回当前是否出了浏览器环境
tiny-invariant
var isProduction = process.env.NODE_ENV === 'production';
var prefix = 'Invariant failed';
function invariant(condition, message) {
if (condition) {
return;
}
if (isProduction) {
throw new Error(prefix);
}
throw new Error(prefix + ": " + (message || ''));
}
export default invariant;
根据是否为true来抛出error
接下里是判断当前浏览器是否支持history.pushState的api
// 是否可以使用history pushState api
const canUseHistory = supportsHistory();
// 是否支持history api
export 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;
}
判断是否需要使用hashChange时间监听浏览器变化
// 是否需要hashChangeListener
const needsHashChangeListener = !supportsPopStateOnHashChange();
/**
* Returns true if browser fires popstate on hash change.
* IE10 and IE11 do not.
* hash更改是会触发popstate
* (
* 直接使用location.hash=就会触发, 但是注意如果hash值相同 使用location.hash= 赋值 不会触发popstate
* 但是使用a标签href= 做锚点则不论是否相等 一定会触发popstate
* )
* 在ie10 和ie11中不会触发
*/
export function supportsPopStateOnHashChange() {
return window.navigator.userAgent.indexOf('Trident') === -1;
}
处理basename
const {
forceRefresh = false, // 是否强制刷新页面
getUserConfirmation = getConfirmation, // 自定义离开当前页面钩子
keyLength = 6 // location.key的长度
} = props;
const basename = props.basename
// 添加第一个字符/ 和去掉最后一个字符/
? stripTrailingSlash(addLeadingSlash(props.basename))
: '';
addLeadingSlash
export function addLeadingSlash(path) {
return path.charAt(0) === '/' ? path : '/' + path;
}
添加basename开头的'/'
stripTrailingSlash
export function stripTrailingSlash(path) {
return path.charAt(path.length - 1) === '/' ? path.slice(0, -1) : path;
}
去掉尾部多余的'/'
接下里创建了一个管理对象,
// 创建管理对象
const transitionManager = createTransitionManager();
初始化location,存储初始化的key
// 初始化location
const initialLocation = getDOMLocation(getHistoryState());
// 所有的key
let allKeys = [initialLocation.key];
getHistoryState 获取当前window.history的state
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/289
return {};
}
}
getDOMLocation
// 获取当前url 生成location
function getDOMLocation(historyState) {
const { key, state } = historyState || {};
const { pathname, search, hash } = window.location;
let path = pathname + search + hash;
// 如果传入basename 并且当前路径不包含basename 则抛出warning
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 +
'".'
);
// 如果有basename 截取path除了basename的部分
if (basename) { path = stripBasename(path, basename); }
return createLocation(path, state, key);
}
获取当前historyState存储的key和state,获取当前浏览器location的pathname search hash
拼接pathname search hash 为path
hasBasename
使用hasBasename判断当前path是否包含basename(开通,且basename后为 / 或 ? 或 #),如果没有包含则抛出warn
export function hasBasename(path, prefix) {
return (
// 判断pat是否一个prefix 开头
path.toLowerCase().indexOf(prefix.toLowerCase()) === 0 &&
// 判断path的prefix之后是否为 / ? #
'/?#'.indexOf(path.charAt(prefix.length)) !== -1
);
}
stripBasename 把path的basename截去
// 截取path除了prefix的部分
export function stripBasename(path, prefix) {
return hasBasename(path, prefix) ? path.substr(prefix.length) : path;
}
使用createLocation创建location
createLocation
// 根据path state生成location
export function createLocation(path, state, key, currentLocation) {
let location;
// 处理两个参数push的情况
if (typeof path === 'string') {
location = parsePath(path);
location.state = state;
// 处理一个参数push的情况
} else {
location = { ...path };
if (location.pathname === undefined) { location.pathname = ''; }
// 补全search ?
if (location.search) {
if (location.search.charAt(0) !== '?') { location.search = '?' + location.search; }
} else {
location.search = '';
}
// 补全hash #
if (location.hash) {
if (location.hash.charAt(0) !== '#') { location.hash = '#' + location.hash; }
} else {
location.hash = '';
}
// 如果state不为undefined 且location的state为undefined 则赋值
if (state !== undefined && location.state === undefined) { location.state = state; }
}
// 转码pathname
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;
}
}
// 如果key存在设置location的key
if (key) { location.key = key; }
if (currentLocation) {
if (!location.pathname) {
location.pathname = currentLocation.pathname;
// 解析不完整的相对路径
} else if (location.pathname.charAt(0) !== '/') {
location.pathname = resolvePathname(
location.pathname,
currentLocation.pathname
);
}
} else {
// 且path为空的情况下设置为/
if (!location.pathname) {
location.pathname = '/';
}
}
return location;
}
这个方法会判断传入的path类型,如果为string类型,则使用parseaPath解析
parsePath
// 解析path, 返回pathname search hash
export 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
};
}
这个方法解析path并返回pathname search hash
解析完path之后,赋值传入的state给location
转码当前的pathname
设置当前location的key
如果传入当前的currentLocation,并且location的pathname不存在,则使用current的pathname
如果location的pathname为相对路径,则拼接当前location的pathname
如果path为不为string,则拷贝当前的path为location,分别补全当前的search,hash,如果传入state,且当前location的state为undefined则赋值state
使用push方法
调用当前的push方法, 首先是容错的判断,如果path为一个存在state不为undefined的object,且传入第二个参数state,则warning
// push函数 使用pushState
function push(path, state) {
warning(
!(
typeof path === 'object' &&
path.state !== undefined &&
state !== undefined
),
// 当地一个参数已经是有state的对象时,不应该使用第二个参数,将被忽略
'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';
// 创建push的location
const location = createLocation(path, state, createKey(), history.location);
// 调用转换confirm
transitionManager.confirmTransitionTo(
location,
action,
getUserConfirmation,
ok => {
if (!ok) { return; }
// 创建完整的href
const href = createHref(location);
// 获取当前location的state 和key
const { key, state } = location;
// 当前环境支持puState api
if (canUseHistory) {
// 使用全局history的pushState 并传入{key, state} 作为state
globalHistory.pushState({ key, state }, null, href);
// 如果强制刷新 则使用location.href直接刷新
if (forceRefresh) {
window.location.href = href;
} else {
// 获取上一个key的index
const prevIndex = allKeys.indexOf(history.location.key);
// 获取之前的keys
const nextKeys = allKeys.slice(0, prevIndex + 1);
// 添加当前的key
nextKeys.push(location.key);
allKeys = nextKeys;
// 重新赋值history 调用监听函数
setState({ action, location });
}
} else {
warning(
state === undefined,
'Browser history cannot push state in browsers that do not support HTML5 history'
);
window.location.href = href;
}
}
);
}
创建当前的action为push,创建location,传入path,state,
使用createKey创建一个key
// 创建key
function createKey() {
return Math.random()
.toString(36)
.substr(2, keyLength);
}
传入当前的location
调用管理对象的confirmTransitionTo,传入callback
当callback参数为false时,则直接return(用户取消跳转)
当为true时使用createHref创建完整的href
createHref 拼接当前的basename
function createHref(location) {
return basename + createPath(location);
}
// 创建完整路径
export function createPath(location) {
const { pathname, search, hash } = location;
let path = pathname || '/';
if (search && search !== '?') { path += search.charAt(0) === '?' ? search : `?${search}`; }
if (hash && hash !== '#') { path += hash.charAt(0) === '#' ? hash : `#${hash}`; }
return path;
}
如果当前支持history.pushState 则调用window.history.pushState方法,使用key和state最为第一个history的state传入
如果forceRefresh参数为true则直接使用window.location.href刷新页面
获取当前的keys,并加入新生成的key,调用setState方法,传入action和location
setState
function setState(nextState) {
// history合并最新的location和action
Object.assign(history, nextState);
// 赋值最新的历史记录属性
history.length = globalHistory.length;
// 调用监听函数
transitionManager.notifyListeners(history.location, history.action);
}
这里同步的把action和location同步给了history,同步了history.length,调用监听函数,传入了location和action
使用replace方法
// replace函数 使用replaceState
function replace(path, state) {
// 当path为一个含有state的location对象,并且第二个state参数有值时,第二个参数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';
// 创建当前location
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) {
// 使用replaceState替换当前href
globalHistory.replaceState({ key, state }, null, href);
if (forceRefresh) {
window.location.replace(href);
} else {
const prevIndex = allKeys.indexOf(history.location.key);
// 使用当前key替换上一个key
if (prevIndex !== -1) { allKeys[prevIndex] = location.key; }
// 重新赋值history,调用监听函数
setState({ action, location });
}
} else {
warning(
state === undefined,
'Browser history cannot replace state in browsers that do not support HTML5 history'
);
window.location.replace(href);
}
}
);
}
与pushState方法不同的是使用了history.replaceState方法,key从push变为替换,直接使用href=变为replace
block/listen/checkDOMListeners
let listenerCount = 0;
// 添加 popstate 和hashchange函数
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;
// 设置block函数
function block(prompt = false) {
// 设置block
const unblock = transitionManager.setPrompt(prompt);
if (!isBlocked) {
// 第一次添加block函数 传入checkDOMListeners 1
checkDOMListeners(1);
// 已添加block
isBlocked = true;
}
// 返回取消block函数
return () => {
if (isBlocked) {
// 设置blocked
isBlocked = false;
// 更改监听content
checkDOMListeners(-1);
}
return unblock();
};
}
// 监听
function listen(listener) {
const unlisten = transitionManager.appendListener(listener);
checkDOMListeners(1);
return () => {
checkDOMListeners(-1);
unlisten();
};
}
checkDOMListeners方法用于添加和移除popState和hashChaneg的事件监听器,巧妙的利用了listenerCount的值,来避免重复添加
block方法设置了管理对象的prompt,调用checkDOMListeners传入1,使用isBlock来避免重复调用,返回一个函数,调用checkDOMListerns传入-1,并调用了管理对象的setPrompt返回的方法
listen方法则是直接调用管理对象添加监听函数,调用checkDOMListeners传入1,返回取消监听函数调用checkDOMListeners传入-1,调用管理对象返回的取消监听函数
浏览器后退、前进、使用go方法触发popstate/hashchange
window.addEventListener(PopStateEvent, handlePopState);
if (needsHashChangeListener) { window.addEventListener(HashChangeEvent, handleHashChange); }
这里可以看到在使用block和listen的时候添加了对于popstate和hashchang的事件监听器(在ie10 ie11 hashchange的back和go不会触发popstate事件)
// popState函数
function handlePopState(event) {
// Ignore extraneous popstate events in WebKit.
if (isExtraneousPopstateEvent(event)) { return; }
// 获取当前浏览器储存的state 生成location
handlePop(getDOMLocation(event.state));
}
// hashchange 函数
function handleHashChange() {
/*
在ie10 ie11 hash的change不会触发popstate,所以需要使用window.history.state获取当前的history.state
*/
handlePop(getDOMLocation(getHistoryState()));
}
两个函数都是使用getDOMLocation生成当前的location传入handlepop函数 不同的是popstate传入的是event的state,hash使用的是window.history.state
isExtraneousPopstateEvent
其中在popstate做了一层容错判断
// 兼容ios的chrome会触发一次无意义的popstate
export function isExtraneousPopstateEvent(event) {
return event.state === undefined && navigator.userAgent.indexOf('CriOS') === -1;
}
handlePop
let forceNextPop = false;
// 处理hashchange 和popState 接收当前的location
function handlePop(location) {
// 如果当前为revertPop 直接setState(没有action和新的location是并没有更改,在这里调用是为了同步history.length)
if (forceNextPop) {
forceNextPop = false;
setState();
} else {
// 声明action
const action = 'POP';
// 调用跳转确认函数
transitionManager.confirmTransitionTo(
location,
action,
getUserConfirmation,
ok => {
// 如果确认跳转 则更改location
if (ok) {
setState({ action, location });
} else {
// 如果不跳转 则revert
revertPop(location);
}
}
);
}
}
这里声明了一个forceNextPop变量为fals,先不管,按照正常流程用户点击后退按钮,这为false,
则声明action为pop,调用管理对象的确认跳转函数,如果为true则直接setState,传入action,location重新赋值history,并调用监听函数
如果为false这里调用了一个reverPop函数传入了location
revertPop
/*
revert函数 场景:在用户点击返回按钮时触发popstate或者hashchange,获取当前的history.state与url生成fromLocation
调用confirm,如果用户点击取消,则返回之前的url,
那么此时的fromLocation是跳转之后根据浏览器url生成的location
toLocation为当前history变量的location,即上一个location
*/
function revertPop(fromLocation) {
const toLocation = history.location;
let toIndex = allKeys.indexOf(toLocation.key);
// 找到前往的key
if (toIndex === -1) { toIndex = 0; }
// 找到来源key
let fromIndex = allKeys.indexOf(fromLocation.key);
if (fromIndex === -1) { fromIndex = 0; }
// 获得revert的层级
const delta = toIndex - fromIndex;
if (delta) {
forceNextPop = true;
go(delta);
}
}
按照用户点击后退然后又点击取消跳转,那么此时的fromLocation为跳转过之后的location,history.location为当前要前往的location,这里使用了key的位置来确定了revert的层级(如果用户点击后退,那么层级必然相差1),更改了forceNextPop为true,又一次使用了go方法跳转
那么此时又会触发pop时间,调用handlePop函数,此时foreNextPop为true,则直接setState,因为此时的history变量并没有改变所以未传值
createHashHistory.js
使用hash路由模式,与browser模式的区别是使用hashPath,即#后边的路由模式,使用hashchange监听路由更改,由于好多东西和browser重复,一样的就简单略过
可以看到同样是暴露出来了一个history对象,和一些方法,我们还是先从初始化开始
初始化
// 是否在浏览器环境
invariant(canUseDOM, 'Hash history needs a DOM');
// 全局history
const globalHistory = window.history;
// 使用history.go是否会触发页面重载
const canGoWithoutReload = supportsGoWithoutReloadUsingHash();
// 跳转确认函数, hash编码方式
const { getUserConfirmation = getConfirmation, hashType = 'slash' } = props;
const basename = props.basename
// 对basename处理,加上开头和结尾的 /
? stripTrailingSlash(addLeadingSlash(props.basename))
: '';
// 拿到编码方式
const { encodePath, decodePath } = HashPathCoders[hashType];
// 创建管理对象
const transitionManager = createTransitionManager();
// 获取当前hashPath
const path = getHashPath();
// 获取当前编码后的hahsPath
const encodedPath = encodePath(path);
// 如果不相等 使用replaceHash替换当前path
/*
* 场景使用默认slash编码方式
* 当前初始化url:/ 则当前hashPath为'' 编码后的hashPath为/
* 则会替换当前hashPath
*/
if (path !== encodedPath) { replaceHashPath(encodedPath); }
// 初始化location
const initialLocation = getDOMLocation();
let allPaths = [createPath(initialLocation)];
判断当前是否在浏览器环境下
拿到window的history
使用history.go是否会导致页面重载
/* Returns false if using go(n) with hash history causes a full page reload.
* 使用history.go()是否会触发页面重载(hash的更改)
*/
export function supportsGoWithoutReloadUsingHash() {
// 应该是在底版的firefox有这个问题
return window.navigator.userAgent.indexOf('Firefox') === -1;
}
这里特别针对firefox判断了一下,应该是低版本的firefox有这个问题
接下来拿到了跳转确认函数,处理了basename,和brwoser是一样的
HashPathCoders
拿到了hash的编码方式
const HashPathCoders = {
hashbang: {
encodePath: path =>
path.charAt(0) === '!' ? path : '!/' + stripLeadingSlash(path),
decodePath: path => (path.charAt(0) === '!' ? path.substr(1) : path)
},
noslash: {
encodePath: stripLeadingSlash,
decodePath: addLeadingSlash
},
slash: {
encodePath: addLeadingSlash,
decodePath: addLeadingSlash
}
};
有这几种编码方式,无非是加了一个!和 带不带/开头
获得当前的hashPath也就是#后边的内容,然后进行转码,判断hashpath和转码之后的不同的话replace了一下
// 获取当前hashPath
const path = getHashPath();
// 获取当前编码后的hahsPath
const encodedPath = encodePath(path);
// 如果不相等 使用replaceHash替换当前path
/*
* 场景使用默认slash编码方式
* 当前初始化url:/ 则当前hashPath为'' 编码后的hashPath为/
* 则会替换当前hashPath
*/
if (path !== encodedPath) { replaceHashPath(encodedPath); }
getHashPath
// 获得当前浏览器的hash 没有使用location.hash 因为在firefox存在编码问题(可以是低版本的)
function getHashPath() {
const href = window.location.href;
const hashIndex = href.indexOf('#');
return hashIndex === -1 ? '' : href.substring(hashIndex + 1);
}
replaceHashPath
// 使用replace方法替换当前hash 直接使用location.hash方法会产生历史记录
function replaceHashPath(path) {
window.location.replace(stripHash(window.location.href) + '#' + path);
}
获取当前location,初始化key
// 初始化location
const initialLocation = getDOMLocation();
let allPaths = [createPath(initialLocation)];
getDOMLocation
getDOMLocation与browser的区别在于获取的是hashPath
// 获取当前的location
function getDOMLocation() {
let path = decodePath(getHashPath());
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 +
'".'
);
// 截去当前basename
if (basename) { path = stripBasename(path, basename); }
return createLocation(path);
}
push流程
function push(path, state) {
warning(
state === undefined,
'Hash history cannot push state; it is ignored'
);
/*
这里可以看到如果传入第二个参数state是被忽略的,但是createLocation并没有判断当前使用的hash模式不赋值state
所以使用path为一个object的情况下还是可以传的,
官方文档说的hash模式不支持state,应该是把state存入当前的history.state加上key,
也就是state和当前路径是绑定的,而hash模式是不使用pushState的
*/
const action = 'PUSH';
const location = createLocation(
path,
undefined,
undefined,
history.location
);
transitionManager.confirmTransitionTo(
location,
action,
getUserConfirmation,
ok => {
if (!ok) { return; }
const path = createPath(location);
// 创建编码后的hashPath
const encodedPath = encodePath(basename + path);
// 比较此次push的path是否更改
const hashChanged = getHashPath() !== encodedPath;
if (hashChanged) {
// 储存当前的path,在触发hashChang的时候标识为push更改的
ignorePath = path;
pushHashPath(encodedPath);
// 找到最后一个当前的path,hash模式是没有生成唯一key的,因为没有pushState第一个参数那样的储存方式
const prevIndex = allPaths.lastIndexOf(createPath(history.location));
const nextPaths = allPaths.slice(0, prevIndex + 1);
// 添加当前的path
nextPaths.push(path);
allPaths = nextPaths;
setState({ action, location });
} else {
warning(
false,
'Hash history cannot PUSH the same path; a new entry will not be added to the history stack'
);
setState();
}
}
);
}
这里不同的地方在于hash模式不支持state参数,因为作者是想把state作为每一个记录的状态储存下来,但是hash模式不像browser模式下可以使用pushState的第一个参数那样可以往全局的history记录状态
这里也是使用createLocation创建location,调用路由跳转的确认
在这里比较了当前hash与即将更改的hash是否相同,如果相同的话,报了个warning,直接setState了一下
如果没有更改的话使用ignorePath变量储存当前的path,标识触发hashchange的是为push更改
使用pushHash更改当前hash
// 使用location.hash更改hash
function pushHashPath(path) {
window.location.hash = path;
}
添加当前的key,与browser不同的是,存储的是当前的hash,并且使用lastIndexOf方法,因为可能存在两个相同的历史
最后使用setState同步history,调用监听函数
replace
function replace(path, state) {
warning(
state === undefined,
'Hash history cannot replace state; it is ignored'
);
const action = 'REPLACE';
const location = createLocation(
path,
undefined,
undefined,
history.location
);
transitionManager.confirmTransitionTo(
location,
action,
getUserConfirmation,
ok => {
if (!ok) { return; }
const path = createPath(location);
const encodedPath = encodePath(basename + path);
const hashChanged = getHashPath() !== encodedPath;
if (hashChanged) {
ignorePath = path;
replaceHashPath(encodedPath);
}
const prevIndex = allPaths.indexOf(createPath(history.location));
if (prevIndex !== -1) { allPaths[prevIndex] = path; }
setState({ action, location });
}
);
}
这里与push方法相似,不同的是使用了replace方法更新了hash,hash未更改的时候也会传入setState方法,应该是为了更换action,添加key的方法变为了替换
浏览器前进/后退、使用go方法触发hashchange
/*
* 在browser模式中很奇怪,hash的更改会触发popstate事件,并且直接使用location.hash=就能触发,
* 更奇怪的是赋值相同的hash 使用location.hash=不会触发popstate,但是使用a.click()相同的hash也能触发
* 但是在hash模式下,hashchang事件完全监听不到相同的hash值赋予,无论是location.hash=还是a.click()
*/
function handleHashChange() {
const path = getHashPath();
const encodedPath = encodePath(path);
// 如果当前path与转码之后不符,则使用replace替换重新触发
if (path !== encodedPath) {
// Ensure we always have a properly-encoded hash.
replaceHashPath(encodedPath);
} else {
const location = getDOMLocation();
const prevLocation = history.location;
// 如果hash更改但是hashPath search hash没有更改则直return
if (!forceNextPop && locationsAreEqual(prevLocation, location)) { return; }
// 如果是push replace触发的hashchange 则直接return
if (ignorePath === createPath(location)) { return; }
ignorePath = null;
handlePop(location);
}
}
这里是先获得当前的hash,判断与转码之后是否相同,不同的话就使用replace重新触发
接下来判断hash是否更改,这里的更改指的是pathname search hash是否更改 因为本来就是hash下的path search hash(但是具测试,相同的hash值使用location.= 和a.click不会触发hashchange)
locationsAreEqual
function locationsAreEqual(a, b) {
return (
a.pathname === b.pathname && a.search === b.search && a.hash === b.hash
);
}
接下来是如果是push或者replace触发的直接return
if (ignorePath === createPath(location)) { return; }
置ignorePath为null
调用handlepop
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);
}
}
);
}
}
这里与browser类似,正常流程forceNextPop为false
调用确认跳转函数,如果确认直接setState,同步history,调用订阅函数
如果为false则调用revertPop
revertPop
function revertPop(fromLocation) {
const toLocation = history.location;
let toIndex = allPaths.lastIndexOf(createPath(toLocation));
if (toIndex === -1) { toIndex = 0; }
let fromIndex = allPaths.lastIndexOf(createPath(fromLocation));
if (fromIndex === -1) { fromIndex = 0; }
const delta = toIndex - fromIndex;
if (delta) {
forceNextPop = true;
go(delta);
}
}
这样与browser类似,fromLocation是当前浏览器获得的location,toLocation是当前声明的history的location(还未同步),只不过找到key的方法改为了lastIndexOf,使用key相差的层级来确定go的层级
再次触发hashchange,注意此时的
// 如果hash更改但是hashPath search hash没有更改则直return
if (!forceNextPop && locationsAreEqual(prevLocation, location)) { return; }
两个location是相同的,但是forceNextPop为true,
触发handlePop直接setState
if (forceNextPop) {
forceNextPop = false;
setState();
other
其他api大致与browser一样,不同的是在使用go的时候会判断是否刷新(firefox)触发警告
function go(n) {
warning(
canGoWithoutReload,
'Hash history go(n) causes a full page reload in this browser'
);
globalHistory.go(n);
}
全文在博主个人博客,由于字数限制 这里删减了后续一部分