popstate 事件
popstate 事件在用户通过浏览器的前进/后退按钮或 JavaScript 的 history.back()、history.forward()、history.go() 方法导航时触发。它是实现 SPA 路由与浏览器历史记录同步的关键事件。本节详解 popstate 事件的触发机制、event.state 的使用方法以及路由监听的完整实现。
前置知识
阅读本节前,建议先了解:history.pushState / replaceState
基础概念
popstate 触发时机
popstate 事件只在浏览器导航操作(前进/后退/go)时触发,不会在 pushState 和 replaceState 调用时触发。
| 操作 | 触发 popstate |
|---|---|
| 点击浏览器后退按钮 | 是 |
| 点击浏览器前进按钮 | 是 |
history.back() | 是 |
history.forward() | 是 |
history.go(-1) | 是 |
history.pushState() | 否 |
history.replaceState() | 否 |
location.href = ... | 否(页面刷新) |
History 栈模型
pushState('/a') pushState('/b') pushState('/c')
│ │ │
▼ ▼ ▼
┌─────┐ ┌─────┐ ┌─────┐
│ /a │ ────────▶ │ /b │ ────────▶ │ /c │ ← 当前位置
└─────┘ └─────┘ └─────┘
栈底 中间 栈顶
用户点击后退:
┌─────┐ ┌─────┐ ┌─────┐
│ /a │ │ /b │ ← 当前 │ /c │
└─────┘ └─────┘ └─────┘
↑ 触发 popstate 事件语法与 API
基本用法
javascript
// 监听 popstate 事件
window.addEventListener('popstate', (event) => {
console.log('触发 popstate');
console.log('状态:', event.state);
console.log('当前 URL:', location.pathname);
});
// 也可以使用 onpopstate 属性
window.onpopstate = (event) => {
console.log('前进/后退被触发');
};event.state
popstate 事件的 state 属性对应 pushState() 或 replaceState() 设置的状态对象:
javascript
// 导航到 /page-a
history.pushState({ page: 'a', title: '页面A' }, '', '/page-a');
// 导航到 /page-b
history.pushState({ page: 'b', title: '页面B' }, '', '/page-b');
// 导航到 /page-c
history.pushState({ page: 'c', title: '页面C' }, '', '/page-c');
// 当前 history.state = { page: 'c', title: '页面C' }
// 监听 popstate
window.addEventListener('popstate', (event) => {
// 点击后退 → event.state = { page: 'b', title: '页面B' }
// 再次后退 → event.state = { page: 'a', title: '页面A' }
console.log('状态:', event.state);
});详细说明
history.state 属性
history.state 返回当前历史记录条目关联的状态对象,无需触发 popstate 就可以读取:
javascript
// 设置状态
history.pushState({ id: 1 }, '', '/page-1');
console.log(history.state); // { id: 1 }
// 再次 pushState
history.pushState({ id: 2 }, '', '/page-2');
console.log(history.state); // { id: 2 }
// replaceState
history.replaceState({ id: 99 }, '', '/page-2-modified');
console.log(history.state); // { id: 99 }popstate 事件的工作流程
javascript
// 完整的 History API 工作流程示例
// 1. 页面加载时初始化
history.replaceState({ page: 'home' }, '', '/home');
window.addEventListener('popstate', handleNavigation);
// 2. 用户点击导航
function navigateTo(page, state) {
history.pushState({ page, ...state }, '', `/${page}`);
renderPage(page);
}
// 3. popstate 处理函数
function handleNavigation(event) {
const state = event.state;
if (state && state.page) {
renderPage(state.page);
} else {
// 无状态对象,回退到首页
renderPage('home');
}
}
// 4. 渲染页面
function renderPage(pageName) {
console.log('渲染页面:', pageName, 'URL:', location.pathname);
// 更新 DOM...
}history.back(), forward(), go()
javascript
// 后退一步
history.back(); // 等同于 history.go(-1)
// 前进一步
history.forward(); // 等同于 history.go(1)
// 前进/后退 N 步
history.go(-2); // 后退两步
history.go(3); // 前进三步
// history.go(0) 刷新当前页面(重新加载)
history.go(0);其他 History 方法
javascript
// 获取历史栈长度
history.length; // 例如: 5
// 注意:无法直接读取历史栈中的 URL 和 state
// 只能通过 popstate 事件间接访问实战示例
完整的 History 路由管理器
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>popstate 路由演示</title>
<style>
body { font-family: -apple-system, sans-serif; padding: 20px; }
.toolbar {
display: flex; gap: 10px; margin-bottom: 20px;
padding: 12px; background: white;
border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.08);
}
.toolbar button {
padding: 8px 16px; border: none; border-radius: 6px;
cursor: pointer; font-size: 13px; font-weight: 600;
background: #e0e0e0; color: #333;
}
.toolbar button:hover { background: #d0d0d0; }
.info-panel {
padding: 16px; background: white;
border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
h2 { color: #1a1a2e; margin-bottom: 16px; }
.state-info {
padding: 12px; background: #f0f4ff; border-radius: 8px;
font-family: monospace; font-size: 13px; margin-top: 16px;
}
.event-log {
height: 200px; overflow-y: auto; padding: 12px;
background: #1a1a2e; border-radius: 8px;
font-family: monospace; font-size: 12px; color: #0f0;
margin-top: 16px;
}
.url-bar {
padding: 10px 14px; background: #f5f5f5;
border: 2px solid #e0e0e0; border-radius: 8px;
font-family: monospace; font-size: 14px;
margin-bottom: 16px; text-align: center;
}
.page-content { margin-top: 16px; line-height: 1.8; }
</style>
</head>
<body>
<h2>popstate 路由演示</h2>
<div class="url-bar" id="urlBar">/home</div>
<div class="toolbar">
<button onclick="navigate('home')">首页</button>
<button onclick="navigate('about')">关于</button>
<button onclick="navigate('blog')">博客</button>
<button onclick="navigate('contact')">联系</button>
<span style="flex:1;"></span>
<button onclick="history.back()">后退</button>
<button onclick="history.forward()">前进</button>
</div>
<div class="info-panel">
<div class="page-content" id="pageContent">首页内容</div>
<div class="state-info" id="stateInfo">history.state: null</div>
<div class="event-log" id="eventLog">事件日志...</div>
</div>
<script>
// 页面数据
const pageData = {
home: { title: '首页', content: '欢迎来到首页。请使用导航按钮切换页面,然后尝试浏览器的前进/后退按钮。' },
about: { title: '关于', content: '这是关于页面。注意观察 popstate 事件日志的变化。' },
blog: { title: '博客', content: '博客列表页面。每次导航都会创建新的历史记录。' },
contact: { title: '联系', content: '联系方式页面。replaceState 不会创建新记录。' }
};
// 导航函数
function navigate(page, replace = false) {
const state = { page, timestamp: Date.now() };
const url = `/${page}`;
if (replace) {
history.replaceState(state, '', url);
addLog('replaceState', url);
} else {
history.pushState(state, '', url);
addLog('pushState', url);
}
renderPage(page);
}
// 渲染页面
function renderPage(page) {
const data = pageData[page] || pageData.home;
document.getElementById('pageContent').innerHTML = `
<h2>${data.title}</h2>
<p>${data.content}</p>
`;
document.getElementById('urlBar').textContent = location.pathname;
updateStateInfo();
}
// 更新状态信息
function updateStateInfo() {
const state = history.state;
document.getElementById('stateInfo').textContent =
`history.state: ${JSON.stringify(state)}\n` +
`history.length: ${history.length}\n` +
`location.pathname: ${location.pathname}`;
}
// 添加日志
function addLog(type, detail) {
const log = document.getElementById('eventLog');
const time = new Date().toLocaleTimeString();
const color = type === 'popstate' ? '#ff0' : type === 'pushState' ? '#64b5f6' : '#ffb74d';
log.innerHTML += `<div style="color:${color}">[${time}] ${type}: ${detail} | state: ${JSON.stringify(history.state)}</div>`;
log.scrollTop = log.scrollHeight;
}
// 监听 popstate
window.addEventListener('popstate', (event) => {
addLog('popstate', location.pathname);
if (event.state && event.state.page) {
renderPage(event.state.page);
} else {
renderPage('home');
}
});
// 初始化
history.replaceState({ page: 'home', timestamp: Date.now() }, '', '/home');
updateStateInfo();
</script>
</body>
</html>注意事项
- 首次加载无 popstate:页面首次加载不会触发
popstate,需要手动初始化状态 - pushState 不触发 popstate:只有导航操作(前进/后退/go)才触发
- state 可能为 null:用户通过其他方式(如直接输入 URL)到达页面时
- replaceState 不触发 popstate:它替换当前记录,但不触发事件
最佳实践
- 页面加载时用
replaceState初始化当前路由状态 - 在
popstate中根据event.state恢复页面状态 event.state为null时提供合理的降级处理- 将路由逻辑封装为独立模块,与页面渲染分离
下一节
继续学习:SPA 路由原理