Description
history 对象
首先,一切的基础是window.history,它是一个只读属性,能够返回一个 History 对象。这个对象提供了接口用于操作浏览器回话历史(即 session history)。这一特性是 HTML5 引入的,叫做 history API。
我们首先需要关注 histroy 的两个属性和 5 个方法,通过这些属性和方法,我们得以用代码获取 history 状态或操作 history:
- length,整个 session history 记录(entry)数量,包括当前加载的页面;
- state,history 栈顶的状态,用于直接获取当前 session 的状态值; 8000
- back(),进入上一页;
- forward(),进入下一页;
- go(),进入指定页,go(1) 等效于 forward(),go(-1) 等效于 back();
- pushState(),添加 history 记录,接受三个参数:state 对象、title(目前浏览器会忽略该参数)、URL(可选);
- replaceState(),替换当前的 history 记录,参数与 pushState() 一样。
接下来还要关注一个事件:popstate。当 hostory 的当前活动记录发生变化时,popstate 事件会被派发给 window。如果被激活的 history 记录是通过 pushState 创建或者被 replaceSate 方法修改过,则事件有一个 state 字段,其值是对应 history 记录的 state 拷贝。但是需要注意,只有调用back()、forward()、go()方法或者用户点击浏览器的“后退”、“前进”按钮才会产生 popstate 事件,通过 pushState 和 replaceSate 函数调用对 history 的操作并不会产生 popstate 事件。
location
另一的基础是 location,它是一个对象,包含当前页面 URL 的相关信息,并且提供了一些方法来修改 URL。通过 window.loaction 和 document.location 都可以访问该对象(window.loaction === window.loaction)。
location 对象拥有以下属性和方法(TODO:简要说明以下属性和方法):
- href
- protocal
- host
- hostname
- port
- pathname
- search
- hash
- username
- password
- origin
- assign()
- reload()
- relace()
- toString()
history 库
history 是一个用于管理 session history 的 JS 库,目前由 ReactTraining 维护。在继续阅读本文之前,最好先去扫一眼 history 库的文档。
history 库的目标是要让你可以在任何能够运行 JS 的地方轻松地管理 session history。那么这里的“任何能够运行 JS 的地方”一般就是指浏览器、Node.js 以及向 React-native 这样的非 DOM 环境。注意:这些环境中只有现代浏览器才支持 HTML5 history API。
它管理 session history 的方法与 HTML5 的 history API 类似,也是提供一个 history 对象。为了在不同的环境中提供相对统一的操作方式,history 库提供了三种创建 history 对象的方法:
- createBrowserHistory,用于支持 HTML5 的现代浏览器环境;
- createMemoryHistory,用于非 DOM 环境;
- createHashHistory,用于老式浏览器。
history 对象与 HTML5 的 history API 很相似,但功能有所加强,具体区别请看它提供的属性和方法:
属性:
- history.length,history 栈中的记录数量
- history.location,当前的 location
- history.action,当前的导航动作
导航方法(请注意方法名与 HTML5 history API 的区别):
- history.push(path, [state])
- history.replace(path, [state])
- history.go(n)
- history.goBack()
- history.goForward()
- history.canGo(n) (仅用于 createMemoryHistory)
监听(这是 HTML5 history API 不提供的):
通过 history.listen 方法可以监听 history.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}`);
});
通过 listen 方法注册的 listener 函数,无论是用户点击“前进”、“后退”按钮或者代码调用导航方法时,还是代码调用push()、replace()方法时,都会被触发。在前面讲 HTML5 的 history API 时提到,只有调用back()、forward()、go()方法或者用户点击“前进”、“后退”按钮时才会产生 popstate 事件,代码中调用 pushState() 和 replaceState() 方法并不会产生 popstate 事件。所以要实现监听,history 不仅要利用 popstate 事件来监听用户点击“前进”、“后退”按钮的行为,还要自己实现一套监听机制来监听代码中对导航方法的调用。
从 history 源码中发现,它实现了一个 TransitionManager 对象,用于管理监听器以及 prompt(在此可以不用关心 prompt 的管理)。去掉 prompt 管理功能之后的代码如下:
// createTransitionManager.js
const createTransitionManager = () => {
let listeners = [];
const appendListener = fn => {
let isActive = true;
const listener = (...args) => {
if (isActive) fn(...args);
};
listeners.push(listener);
return () => {
isActive = false;
listeners = listeners.filter(item => item !== listener);
};
};
const notifyListeners = (...args) => {
listeners.forEach(listener => listener(...args));
};
return {
appendListener,
notifyListeners
};
};
export default createTransitionManager;
然后再来看 history.listen() 方法的实现:
const listen = listener => {
const unlisten = transitionManager.appendListener(listener);
checkDOMListeners(1);
return () => {
checkDOMListeners(-1);
unlisten();
};
};
对这段代码代码做一个简要解释:
首先将 listener 添加到 transitionManager 中去。另外还有对 checkDOMListeners 的函数的调用,这个函数实现的逻辑有点令人不解,而且做法不太科学,存在潜在的问题,在此就不对它进行分析了,只说它的作用。history 代码中实现了一个名为 handlePopState 的函数,用于监听最开始提到的 popstate 事件,checkDOMListeners(1)是为了确保当 transitionManager 对象中注册的 listener 数量不为 0 时该函数被注册 listener 了,checkDOMListeners(-1) 的作用是确保当 transitionManager 对象中注册的 listener 数量为 0 时该函数从事件监听上移除。
再来看 handlePopState 函数:
const handlePopState = event => {
// Ignore extraneous popstate events in WebKit.
if (isExtraneousPopstateEvent(event)) return;
handlePop(getDOMLocation(event.state));
};
该函数最后调用了名为 handlePop 的函数,再继续看这个函数:
const 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);
}
}
);
}
};
先分析这个函数的作用:经过 confirmTransitionTo 抉择(这属于支线剧情,暂时不关心)之后,最终调用 setState 函数,setState 函数的实现如下:
const setState = nextState => {
Object.assign(history, nextState);
history.length = globalHistory.length;
transitionManager.notifyListeners(history.location, history.action);
};
在这个函数里,我们通过 history.listen 注册的所有监听器终于被触发了。这就说明,popstate 事件的监听函数,最终会调用我们通过 history.listen 注册的所有监听器,也就是说 history.listen() 达到了监听 popstate 事件的效果。
至此,已经清楚 history 如何利用 popstate 事件来监听用户点击“前进”、“后退”按钮或者代码里的导航动作,接下来继续探究它如何监听用户代码对 push 和 replace 的调用。
通过寻找 setState 函数被调用的地方发现,history 对象的 push 和 replace 函数体中都调用了 setState,那这就很明了了:调用 push 和 replace 也能触发通过 history.listen 注册的所有监听器。
接下来分析其中的细节。
loaction:
// createBrowserHistory.js
// ...
const createBrowserHistory = (props = {}) => {
const history = {
length: globalHistory.length,
action: "POP",
location: initialLocation,
createHref,
push,
replace,
go,
goBack,
goForward,
block,
listen
};
return history;
};
从 createBrowserHistory() 函数返回的 history 对象中,location 属性包含了最多的信息,现在来看看 location 对象里有些什么数据。文档中已经有说明:
- location 对象实现 window.loaction 接口的子集,包括这些属性:
- location.pathname,URL 的 path 部分
- location.search,Query string
- location.hash,URL的 hash 段
- location.state, 一些无法在 URL 中体现的额外信息(只支持 createBrowserHistory 和
createMemoryHistory) - location.key, 代表该 location 的唯一字符串 (只支持 createBrowserHistory 和 createMemoryHistory)
如果是在支持 HTML5 story API 的现代浏览器环境下,location 中的字段其实是将 window.history.state 和 window.location 这两个对象的数据合并之后的结果。
TODO:待续