history源码阅读笔记
history为react-router的主要依赖项之一,用于操纵浏览器url的位置跳转
1. 浏览器相关API介绍
Location
location是最有用的BOM(浏览器对象模型)对象之一,提供了与当前窗口中加载的文档有关信息,还提供了一些导航功能。location对象即是window对象的属性,也是document对象的属性。故有:
window.location === document.location // true
Location
对象包含的属性及方法
// 以
// http://anonymous:flabada@www.xujsp.com:8080/javascript/doc?query=location#fragment
// 为例
// 页面完整URL
href: 'http://anonymous:wordpass@www.xujsp.com:8080/javascript/doc?query=location#fragment',
// 页面使用的协议
protocol: 'http',
// 服务器名称及端口号
host: 'www.xujsp.com:8080',
// 服务器名称
hostname: 'www.xujsp.com',
// 路径
pathname: '/javascript/doc',
// 查询字符串
search: '?query=location',
// hash
hash: '#fragment',
// 用户登录用户名
username: 'anonymous',
// 用户登录信息密码
password: 'wordpass',
}
-
修改
location
属性(除hash
)外,页面都会以新URL重新加载,并生成一条新记录 -
通过使用
location.replace()
方法可以禁止生成新记录,此时用户不能通过“后退”按钮导航到前一个页面 -
location.reload()
强制页面重新加载,不会生成记录 -
location.assign()
打开行URL,并在浏览器中生成新记录
event: hashchange
-
HTML5中新增的
hashchange
事件,在URL参数列表发生变化时触发。便于开发人员利用URL参数列表来保存状态及导航信息 -
hashchange
事件处理程序添加到window
对象 -
对应的
event
对象包含oldURL
和newURL
对象,分别保存参数列表变化前后的完整URL
History
-
History
接口用于操作浏览器会话历史记录
History
包含的属性及方法
{
// 会话历史中元素的数目
length: number,
state: object,
}
-
history.pushState()
将新的状态信息加入历史状态栈,包括状态对象和相对url,但不会向服务器发出请求 -
history.replaceState()
重写当前状态,不会在历史状态栈中创建状态
event: popState
-
当点击后退、前进按钮或调用
history.back()
、history.forward()
或history.go()
时触发 -
history.pushState()
和history.pushState()
不会触发popState
事件 -
对应的
event
对象包含一个state
属性,为pushState
函数传入的第一个参数 -
浏览器加载的第一个页面没有状态,此时
event.state === null
2. history API简介
history提供一系列API,用于管理历史堆栈、导航、确认以及在会话之间保持状态
-
history 提供了三个不同方法来创建
history
对象 -
createBrowserHistory
使用 HTML5 history API 实现 -
createMemoryHistory
用于非DOM环境中,如React-native -
createHashHistory
相对于createBrowserHistory
兼容性更好
属性
history
对象包含以下属性
-
history.length
:历史堆栈元素数量
-
history.location
:当前location
-
history.action
:当前导航的动作形式:
enum {'PUSH', 'REPLACE', 'POP'}
,取决于用户操作
此外
createMemoryHistory
还提供了
history.index
和
history.entries
来查看历史堆栈
监听
可以通过
history.listen
监听当前
location
的变化
history.listen((location, action) => {
console.log(
`The current URL is ${location.pathname}${location.search}${location.hash}`
console.log(`The last navigation action was ${action}`)
location
对象实现了
window.location
接口的子集,包括
-
location.pathname
:URL路径
-
location.search
:查询字符串
-
location.hash
:hash
Locations可能包含以下属性
-
location.state
:为当前location设置一些额外状态(
createBrowserHistory
和
createMemoryHistory
支持)
-
location.key
:当前location的唯一字符串(
createBrowserHistory
和
createMemoryHistory
支持)
导航
history
可以使用以下方法修改当前location
-
history.push(path, [state])
-
history.replace(path, [state])
-
history.go(n)
-
history.goBack()
-
history.goForward()
-
history.canGo(n)
(仅在
createMemoryHistory
中)
在使用
push
或
replace
时,可以将URL路径和状态以不同的参数指定,也可以将所有内容包含在单个对象中:
history.push("/home")
// Location 状态不会出现在URL中
history.push("/home?the=query", { some: "state" })
// 同上
history.push({
pathname: "/home",
search: "?the=query",
state: { some: "state" }
// 返回到前一个状态,两者功能相同
history.go(-1)
history.goBack()
阻塞导航变换
history
可以注册一个提示消息,会在location监听函数收到通知之前显示给用户。可以确保用户在离开之前确认是否离开当前页面。
// 注册一条提示消息,在用户离开当前页面之前将显示给用户。
const unblock = history.block("Are you sure you want to leave this page?")
// 也可以是一个函数
history.block((location, action) => {
// location和action参数指明如何导航到下一个location
// 通常在用户离开当前页面时尚未提交表单
if (input.value !== "") return "Are you sure you want to leave this page?"
// 停止阻塞
unblock()
注意: 在
createMemoryHistory
中,需要提供一个
getUserConfirmation
函数来使用此特性
自定义确认对话框
默认情况下,使用
window.confirm
向用户显示提示消息。如果您需要重写此行为(或者如果您使用的是不假定DOM环境的createMemoryHistory),需要在创建历史记录对象时提供getUserConfirmation函数。
const history = createHistory({
getUserConfirmation(message, callback) {
// 显示自定义对话框,
// callback(true)继续
// callback(false)停止变换
使用"base" URL
如果应用中的所有URLs都与其他“base”URL相关,使用
basename
选项。该选项将给定的字符串添加到使用的所有URL的前面。
const history = createHistory({
basename: "/the/base"
history.listen(location => {
console.log(location.pathname) // /home
history.push("/home") // URL为/the/base/home
注意:
createMemoryHistory
不支持
basename
在createBrowserHistory中强制网页刷新
默认情况下,
createBrowserHistory
使用HTML5中的
pushState
和
replaceState
来防止在浏览时从服务器重新加载整个页面。如果您想在URL更改时重新加载,使用forceRefresh选项。
const history = createBrowserHistory({
forceRefresh: true
修改
createHashHistory
中的hash类型
默认情况下,
createHashHistory
在基于hash的URL中使用前导斜杠。您可以使用hashType选项来使用不同的hash格式。
const history = createHashHistory({
hashType: "slash" // 默认值
history.push("/home") // window.location.hash is #/home
const history = createHashHistory({
hashType: "noslash" // 忽略前导斜杠
history.push("/home") // window.location.hash is #home
const history = createHashHistory({
hashType: "hashbang" // google已弃用
history.push("/home") // window.location.hash is #!/home
3. 源码解析(version: 4.7.2)
PathUtil.js
用于操作path
// 添加和去除前导'/'
export var addLeadingSlash = function addLeadingSlash(path) {
return path.charAt(0) === '/' ? path : '/' + path;
export var stripLeadingSlash = function stripLeadingSlash(path) {
return path.charAt(0) === '/' ? path.substr(1) : path;
// 检查path是否以${prefix}开头
// 后缀可能为/ ? #或者path === prefix
export var hasBasename = function hasBasename(path, prefix) {
return new RegExp('^' + prefix + '(\\/|\\?|#|$)', 'i').test(path);
// 如果path以prefix,删除prefix
export var stripBasename = function stripBasename(path, prefix) {
return hasBasename(path, prefix) ? path.substr(prefix.length) : path;
// 删除末尾'/'
export var stripTrailingSlash = function stripTrailingSlash(path) {
return path.charAt(path.length - 1) === '/' ? path.slice(0, -1) : path;
// 将path解析为pathname、search、hash
export var parsePath = function parsePath(path) {
var pathname = path || '/';
var search = '';
var hash = '';
// url的hash以#开头,并处于url最后部分
var hashIndex = pathname.indexOf('#');
if (hashIndex !== -1) {
hash = pathname.substr(hashIndex);
pathname = pathname.substr(0, hashIndex);
// 查询字符串在hash的前面
var searchIndex = pathname.indexOf('?');
if (searchIndex !== -1) {
search = pathname.substr(searchIndex);
pathname = pathname.substr(0, searchIndex);
return {
pathname: pathname,
search: search === '?' ? '' : search,
hash: hash === '#' ? '' : hash
// 根据Location中的pathname、search、hash生成path
export var createPath = function createPath(location) {
var pathname = location.pathname,
search = location.search,
hash = location.hash;
var path = pathname || '/';
if (search && search !== '?') path += search.charAt(0) === '?' ? search : '?' + search;
if (hash && hash !== '#') path += hash.charAt(0) === '#' ? hash : '#' + hash;
return path;
LocationUtil.js
用于生成Location及判断Location是否相等
var _extends = Object.assign || function (target) {
for (var i = 1; i < arguments.length; i++) {
var source = arguments[i];
for (var key in source) {
if (Object.prototype.hasOwnProperty.call(source, key)) {
target[key] = source[key];
return target;
// 以第二参数为基准,默认为'/',解析第一个参数得到最终路径
import resolvePathname from 'resolve-pathname';
// 利用Object.prototype.valueOf比较是否相等
import valueEqual from 'value-equal';
import { parsePath } from './PathUtils';
// 创建location对象,包含pathname,search,hash,state,key
export var createLocation = function createLocation(path, state, key, currentLocation) {
var location = void 0;
if (typeof path === 'string') {
// Two-arg form: push(path, state)
// 参数格式为(path: String, state: Object)
location = parsePath(path);
location.state = state;
} else {
// 参数格式为(path: Object),基于Location对象创建location
location = _extends({}, 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) {
// decodeURI在解析非合法的URI编码是抛出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) {
// 相对于currentLocation解析location.pathname
if (!location.pathname) {
location.pathname = currentLocation.pathname;
} else if (location.pathname.charAt(0) !== '/') {
location.pathname = resolvePathname(location.pathname, currentLocation.pathname);
} else {
// 如果path为空且没有currentLocation,将location.pathname设置为/
if (!location.pathname) {
location.pathname = '/';
return location;
// 判断标准为pathname、search、hash及state对象都相等
export var locationsAreEqual = 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);
DOMUtils.js
判断浏览器对相关API的支持情况
// 是否为浏览器环境
export var canUseDOM = !!(
typeof window !== 'undefined'
&& window.document
&& window.document.createElement
// 添加及删除监听函数,同时适用于IE及chrome
export var addEventListener = function addEventListener(node, event, listener) {
return node.addEventListener
? node.addEventListener(event, listener, false)
: node.attachEvent('on' + event, listener);
export var removeEventListener = function removeEventListener(node, event, listener) {
return node.removeEventListener
? node.removeEventListener(event, listener, false)
: node.detachEvent('on' + event, listener);
export var getConfirmation = function getConfirmation(message, callback) {
return callback(window.confirm(message));
}; // eslint-disable-line no-alert
// 判断是否支持HTML5 history API
export var supportsHistory = function supportsHistory() {
var 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;
* Returns true if browser fires popstate on hash change.
* IE10 and IE11 do not.
// 判断hash改变时是否触发popstate事件,IE10和IE11不支持
export var supportsPopStateOnHashChange = function supportsPopStateOnHashChange() {
return window.navigator.userAgent.indexOf('Trident') === -1;
// todo: 以下两种情况尚未尝试
* Returns false if using go(n) with hash history causes a full page reload.
export var supportsGoWithoutReloadUsingHash = function supportsGoWithoutReloadUsingHash() {
return window.navigator.userAgent.indexOf('Firefox') === -1;
* Returns true if a given popstate event is an extraneous WebKit event.
* Accounts for the fact that Chrome on iOS fires real popstate events
* containing undefined state when pressing the back button.
export var isExtraneousPopstateEvent = function isExtraneousPopstateEvent(event) {
return event.state === undefined && navigator.userAgent.indexOf('CriOS') === -1;
createTransitionManager.js
控制路由跳转及添加路由跳转监听函数
// warning(condition: bool, messsage: String):当condition为false时,打印警告信息
import warning from 'warning';
var createTransitionManager = function createTransitionManager() {
var prompt = null;
// 设置prompt,并返回清空函数,参数为string或function
var setPrompt = function setPrompt(nextPrompt) {
warning(prompt == null, 'A history supports only one prompt at a time');
prompt = nextPrompt;
return function () {
if (prompt === nextPrompt) prompt = null;
var confirmTransitionTo = 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) {
// 如果prompt不为空,计算提示信息
var 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.
// 如果提示信息不是字符串,result为false,取消跳转,其余情况跳转
callback(result !== false);
} else {
// 如果prompt为空,callback(true)直接调转
callback(true);
// 存储监听函数
var listeners = [];
// 添加监听函数
var appendListener = function appendListener(fn) {
// isActive 用于指示是否真正调用监听函数fn
// 在使用notifyListeners依次调用监听函数时,
// 如果在某个监听函数中取消其他监听函数时,可以防止对应监听函数被调用
var isActive = true;
var listener = function listener() {
if (isActive) fn.apply(undefined, arguments);
listeners.push(listener);
// 返回取消监听函数
return function () {
isActive = false;
listeners = listeners.filter(function (item) {
return item !== listener;
// 依次调用监听函数
var notifyListeners = function notifyListeners() {
for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) {
args[_key] = arguments[_key];
listeners.forEach(function (listener) {
return listener.apply(undefined, args);
return {
setPrompt: setPrompt,
confirmTransitionTo: confirmTransitionTo,
appendListener: appendListener,
notifyListeners: notifyListeners
export default createTransitionManager;
createBrowserHistory.js
创建基于HTML5 history API的自定义history对象
// 使_typeof可以支持判断Symbol类型
var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol"
? function (obj) { return typeof obj; }
: function (obj) {
return obj
&& typeof Symbol === "function"
&& obj.constructor === Symbol
&& obj !== Symbol.prototype ? "symbol" : typeof obj;
// Object.assign垫片函数
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
// 当条件不满足时,打印警告信息
// warning(condition: bool, message: string)
import warning from 'warning';
// 当条件不满足时,抛出错误信息
// invariant(condition: bool, message: string)
import invariant from 'invariant';
import { createLocation } from './LocationUtils';
import { addLeadingSlash, stripTrailingSlash, hasBasename, stripBasename, createPath } from './PathUtils';
import createTransitionManager from './createTransitionManager';
import { canUseDOM, addEventListener, removeEventListener, getConfirmation, supportsHistory, supportsPopStateOnHashChange, isExtraneousPopstateEvent } from './DOMUtils';
var PopStateEvent = 'popstate';
var HashChangeEvent = 'hashchange';
// 获取history.state 等价于popstate事件中的ev.state
var getHistoryState = 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 {};
* Creates a history object that uses the HTML5 history API including
* pushState, replaceState, and the popstate event.
// 利用HTML5 history API创建history对象
var createBrowserHistory = function createBrowserHistory() {
var props = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
// 确保支持DOM操作,在浏览器环境中
invariant(canUseDOM, 'Browser history needs a DOM');
var globalHistory = window.history;
var canUseHistory = supportsHistory();
// 当hash改变时,如果不能触发popstate事件,则添加hashchange事件
var needsHashChangeListener = !supportsPopStateOnHashChange();
// 是否强制刷新
var _props$forceRefresh = props.forceRefresh,
forceRefresh = _props$forceRefresh === undefined ? false : _props$forceRefresh,
// 自定义用户确认函数,默认为 (message, callback) => callback(window.confirm(message))
_props$getUserConfirm = props.getUserConfirmation,
getUserConfirmation = _props$getUserConfirm === undefined ? getConfirmation : _props$getUserConfirm,
// 随机key的长度,默认为6
_props$keyLength = props.keyLength,
keyLength = _props$keyLength === undefined ? 6 : _props$keyLength;
// baseurl 转换为 /name 格式
var basename = props.basename ? stripTrailingSlash(addLeadingSlash(props.basename)) : '';
var getDOMLocation = function getDOMLocation(historyState) {
var _ref = historyState || {},
key = _ref.key,
state = _ref.state;
// 获取当前location中的pathname、search、hash
var _window$location = window.location,
pathname = _window$location.pathname,
search = _window$location.search,
hash = _window$location.hash;
// 拼接path
var path = pathname + search + hash;
// 如果当前path不以basename为前缀,打印警告信息
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);
// 创建location
return createLocation(path, state, key);
// 生成随机key
var createKey = function createKey() {
return Math.random().toString(36).substr(2, keyLength);
var transitionManager = createTransitionManager();
// 在路由发生跳转是,更新history中的部分属性,如action,location等
// 路由跳转完成后,触发所有监听函数
var setState = function setState(nextState) { // nextState: { action, location }
_extends(history, nextState);
history.length = globalHistory.length;
transitionManager.notifyListeners(history.location, history.action);
// event.state包含key和传入的state两部分内容
// handleHashChange为handlePopState的辅助监听函数,用于处理IE10和IE11在hash改变时,不能触发popstate事件的备选方案
var handlePopState = function handlePopState(event) {
// Ignore extraneous popstate events in WebKit.
// 当触popstate事件时,event.state属性不能被正确支持时,直接返回
if (isExtraneousPopstateEvent(event)) return;
handlePop(getDOMLocation(event.state));
var handleHashChange = function handleHashChange() {
handlePop(getDOMLocation(getHistoryState()));
// forceNextPop用于popstate事件是否是用于用户拒绝跳转导致触发的再次触发popstate事件
var forceNextPop = false;
// 当触发popstate事件时,此时页面已跳转完成,location记录当前页面的相关信息
var handlePop = function handlePop(location) {
// forceNextPop为true,说明此时为用户拒绝跳转后的popstate触发,此时的setState()作用为调用其中的监听函数,此时的action仍为前一个action,不一定为pop
if (forceNextPop) {
forceNextPop = false;
setState();
} else {
var action = 'POP';
// 弹出跳转确认信息时,此时页面已经跳转
// 如果用户确定跳转,则更新history中action和location信息
// 如果用户取消跳转,则需要通过revertPop重新跳转至前一个页面
transitionManager.confirmTransitionTo(location, action, getUserConfirmation, function (ok) {
if (ok) {
setState({ action: action, location: location });
} else {
revertPop(location);
var revertPop = function revertPop(fromLocation) {
// history.location仍保持popstate事件触发前的location信息
var 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.
// 通过计算fromLocation.key和toLocation.key在allKey中的索引差值,作为是否调转的标准
var toIndex = allKeys.indexOf(toLocation.key);
if (toIndex === -1) toIndex = 0;
var fromIndex = allKeys.indexOf(fromLocation.key);
if (fromIndex === -1) fromIndex = 0;
var delta = toIndex - fromIndex;
if (delta) {
// go的调用会再次触发popstate事件
// 因此,当用户拒绝跳转时,handlePop会被调用两次
forceNextPop = true;
go(delta);
// 初始情况下的getHistoryState()通常为{}, History.state 为null
var initialLocation = getDOMLocation(getHistoryState());
var allKeys = [initialLocation.key];
// Public interface
// 生成完整链接
var createHref = function createHref(location) {
return basename + createPath(location);
var push = function push(path, state) {
// 当path为包含state属性的对象时,不应该在传入state参数,此时state会被忽略
warning(
!((typeof path === 'undefined' ? 'undefined' : _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',
var action = 'PUSH';
// 创建新的location
var location = createLocation(path, state, createKey(), history.location);
// (location, action, getUserConfirmation, callback)
transitionManager.confirmTransitionTo(location, action, getUserConfirmation, function (ok) {
if (!ok) return;
// callback(true)跳转
var href = createHref(location);
var key = location.key,
state = location.state;
if (canUseHistory) {
// 支持history时,使用pushState
globalHistory.pushState({ key: key, state: state }, null, href);
if (forceRefresh) {
window.location.href = href;
} else {
// 更新存储的allKeys
// 当location处于history队尾时,实际为push
// 当location处于history中间时,会删除之后的keys,并添加新key
var prevIndex = allKeys.indexOf(history.location.key);
var nextKeys = allKeys.slice(0, prevIndex === -1 ? 0 : prevIndex + 1);
nextKeys.push(location.key);
allKeys = nextKeys;
// 更新history,并触发监听函数
setState({ action: action, location: location });
} else {
// 不支持history API时,在包含state的情况下,打印警告信息,并使用location实现跳转
warning(state === undefined, 'Browser history cannot push state in browsers that do not support HTML5 history');
window.location.href = href;
// 与push基本相同
// 唯一的区别在于使用history的replaceState替换当前path,避免添加历史记录
var replace = function replace(path, state) {
warning(!((typeof path === 'undefined' ? 'undefined' : _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');
var action = 'REPLACE';
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.replaceState({ key: key, state: state }, null, href);
if (forceRefresh) {
window.location.replace(href);
} else {
var prevIndex = allKeys.indexOf(history.location.key);
if (prevIndex !== -1) allKeys[prevIndex] = location.key;
setState({ action: action, location: location });
} else {
// 在不支持history API时,使用Location.replace实现替换功能
warning(state === undefined, 'Browser history cannot replace state in browsers that do not support HTML5 history');
window.location.replace(href);
// 前进后退
var go = function go(n) {
globalHistory.go(n);
var goBack = function goBack() {
return go(-1);
var goForward = function goForward() {
return go(1);
var listenerCount = 0;
var checkDOMListeners = function checkDOMListeners(delta) {
// 修改监听计数器数量,
listenerCount += delta;
// 如果监听计数器为1,添加handlePopState处理函数
// 如果监听计数器为0,删除handlePopState处理函数
// 其中的handleHashChange函数,用于处理IE10和IE11在hash改变时,不能触发popstate事件的备选方案
if (listenerCount === 1) {
addEventListener(window, PopStateEvent, handlePopState);
if (needsHashChangeListener) addEventListener(window, HashChangeEvent, handleHashChange);
} else if (listenerCount === 0) {
removeEventListener(window, PopStateEvent, handlePopState);
if (needsHashChangeListener) removeEventListener(window, HashChangeEvent, handleHashChange);
var isBlocked = false;
// block被调用时会修改监听计数器数量listenerCount,
// 只有当listenerCount不为0时,才会监听popstate事件,以确保popstate时对应逻辑得以执行,包括弹出用户确认框及调用监听函数
// 如果block和listen函数都没有调用,则listernCount为0
// 则不存在任何注册事件,仅执行浏览器默认动作:更新路由
var block = function block() {
// 参数不存在时,prompt为false,此时transitionManager会直接阻止跳转
var prompt = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
// unblock用于清空prompt,prompt为null时,直接跳转,unblock函数不会返回任何值
var unblock = transitionManager.setPrompt(prompt);
// 没有阻塞时,监听计数器加一,并将isBlocked记为true,说明存在Block
// 第二次调用block函数时,如果未调用block返回的函数,则会重新设置prompt信息,并给出警告,不会产生其他效果
if (!isBlocked) {
checkDOMListeners(1);
isBlocked = true;
// 返回的函数用于取消block信息,并将监听计数器减一
return function () {
if (isBlocked) {
isBlocked = false;
checkDOMListeners(-1);
return unblock();
// 添加自定义监听函数,并返回取消监听函数
var listen = function listen(listener) {
var unlisten = transitionManager.appendListener(listener);
checkDOMListeners(1);
return function () {
checkDOMListeners(-1);
unlisten();
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