Skip to content

popstate 事件

popstate 事件在用户通过浏览器的前进/后退按钮或 JavaScript 的 history.back()history.forward()history.go() 方法导航时触发。它是实现 SPA 路由与浏览器历史记录同步的关键事件。本节详解 popstate 事件的触发机制、event.state 的使用方法以及路由监听的完整实现。

前置知识

阅读本节前,建议先了解:history.pushState / replaceState

基础概念

popstate 触发时机

popstate 事件只在浏览器导航操作(前进/后退/go)时触发,不会在 pushStatereplaceState 调用时触发。

操作触发 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>

注意事项

  1. 首次加载无 popstate:页面首次加载不会触发 popstate,需要手动初始化状态
  2. pushState 不触发 popstate:只有导航操作(前进/后退/go)才触发
  3. state 可能为 null:用户通过其他方式(如直接输入 URL)到达页面时
  4. replaceState 不触发 popstate:它替换当前记录,但不触发事件

最佳实践

  1. 页面加载时用 replaceState 初始化当前路由状态
  2. popstate 中根据 event.state 恢复页面状态
  3. event.statenull 时提供合理的降级处理
  4. 将路由逻辑封装为独立模块,与页面渲染分离

下一节

继续学习:SPA 路由原理

参考链接