解释 Vue Router 源码中 `history` 对象的 `listen` 和 `unlisten` 机制,以及它们如何与浏览器历史栈交互。

各位靓仔靓女,晚上好!我是今晚的讲师,咱们今天的主题是:Vue Router 源码里的 history 对象,重点聊聊它的 listenunlisten 这哥俩,以及它们怎么跟浏览器的历史栈眉来眼去的。

准备好了吗?咱们要开车了!

开场白:history 是个啥?

在 Vue Router 的世界里,history 对象可不是指你过去的光辉岁月,而是指浏览器提供的历史记录接口。它允许我们访问浏览器的会话历史,并且可以操作它,比如前进、后退、添加新的历史记录等等。

Vue Router 需要利用 history 对象来实现单页应用 (SPA) 的路由功能,这样才能在不刷新页面的情况下,切换不同的“页面”。

history 的几种姿势

在 Vue Router 中,我们常见的 history 实现方式有三种:

  • HTML5 History Mode (createWebHistory): 使用 history.pushStatehistory.replaceState 来操作 URL,URL 看起来跟正常的网站一样,没有 # 号。 这是推荐的姿势。
  • Hash Mode (createWebHashHistory): 使用 URL 中的 # 号来模拟路由。兼容性好,但 URL 比较丑。
  • Memory History Mode (createMemoryHistory): 不与浏览器 URL 交互,路由信息存储在内存中。主要用于服务端渲染 (SSR) 和测试环境。

咱们今天主要讲 HTML5 History Mode,因为它最常用,也最能体现 listenunlisten 的作用。

listen: 监听历史记录变化的小雷达

history 对象本身提供了一些事件,比如 popstate 事件,当用户点击浏览器的前进/后退按钮时,这个事件会被触发。但是,Vue Router 需要更精细的控制,它需要知道每一次路由变化,并执行相应的操作 (比如更新组件、滚动到页面顶部等等)。

所以,Vue Router 自己封装了一个 listen 函数,用于监听历史记录的变化。这个 listen 函数本质上是一个观察者模式的实现。

// 简化版的 listen 函数 (来自 Vue Router 源码的灵感)
function createHistoryListener() {
  let listeners = [];

  function listen(callback) {
    listeners.push(callback);
    return () => { // 返回 unlisten 函数
      listeners = listeners.filter(cb => cb !== callback);
    };
  }

  function trigger(location, state) {
    for (const listener of listeners) {
      listener(location, state);
    }
  }

  return {
    listen,
    trigger,
  };
}

const historyListener = createHistoryListener();

// 注册监听器
const unlisten = historyListener.listen((location, state) => {
  console.log('路由变化了!', location, state);
  // 在这里执行路由更新的操作
});

// 模拟路由变化
historyListener.trigger({ path: '/about' }, { data: 'some data' });

// 取消监听
unlisten();

// 再次模拟路由变化 (不会触发监听器)
historyListener.trigger({ path: '/contact' }, { data: 'other data' });

上面的代码模拟了一个简单的 listen 函数。它内部维护了一个 listeners 数组,用于存储所有的回调函数。

  • listen(callback): 接收一个回调函数 callback,并将它添加到 listeners 数组中。 它返回一个 unlisten 函数,用于取消监听。
  • trigger(location, state): 当路由发生变化时,调用 trigger 函数,它会遍历 listeners 数组,并依次执行每个回调函数,并将新的 locationstate 作为参数传递给回调函数。

Vue Router 源码里的 listen

Vue Router 的 history 对象里也有类似的 listen 函数。 它的作用是:

  1. 监听浏览器的 popstate 事件: 当用户点击浏览器的前进/后退按钮时,触发 popstate 事件。
  2. 调用 trigger 函数:popstate 事件的回调函数中,调用 trigger 函数,通知所有注册的监听器。
// Vue Router 源码片段 (简化)

// 创建 HTML5 History
function createWebHistory(base) {
  //...

  const history = {
    //...
    listen: (callback) => {
      listeners.push(callback);
      return () => {
        remove(listeners, callback);
      };
    },
    //...
  }

  // 监听 popstate 事件
  window.addEventListener('popstate', (event) => {
    //...
    for (const listener of listeners) {
      listener(location, state);
    }
    //...
  });

  return history;
}

unlisten:取消监听的橡皮擦

unlisten 函数是 listen 函数的返回值。 它的作用是:

  1. listeners 数组中移除对应的回调函数。
  2. 防止内存泄漏: 如果不再需要监听路由变化,就应该及时调用 unlisten 函数,否则回调函数会一直保存在内存中,导致内存泄漏。
// 再次回顾简化版的 listen 函数
function createHistoryListener() {
  let listeners = [];

  function listen(callback) {
    listeners.push(callback);
    return () => { // 返回 unlisten 函数
      listeners = listeners.filter(cb => cb !== callback);
    };
  }

  function trigger(location, state) {
    for (const listener of listeners) {
      listener(location, state);
    }
  }

  return {
    listen,
    trigger,
  };
}

在 Vue Router 源码中,unlisten 函数的实现也类似,就是从 listeners 数组中移除对应的回调函数。

listenunlisten 如何与浏览器历史栈交互?

listenunlisten 并不是直接与浏览器历史栈交互,而是与浏览器的 popstate 事件交互。

当用户点击浏览器的前进/后退按钮时,浏览器会触发 popstate 事件。 Vue Router 的 history 对象会监听 popstate 事件,并在事件回调函数中,调用 trigger 函数,通知所有注册的监听器。

这些监听器 (比如 Vue Router 的内部路由逻辑) 会根据新的 URL,更新组件、滚动到页面顶部等等,从而实现路由切换的效果。

表格总结 listenunlisten

特性 listen unlisten
作用 注册一个回调函数,监听历史记录的变化。 取消监听,从监听列表中移除回调函数。
参数 回调函数 (接收 location 和 state 作为参数) 无参数
返回值 unlisten 函数 无返回值
触发时机 每次历史记录发生变化时 (通过 trigger 函数) 当你不再需要监听路由变化时,手动调用。
与浏览器交互 间接通过 popstate 事件。 无直接交互。
重要性 实现路由监听的核心函数。 防止内存泄漏,保持应用性能的关键函数。

代码示例:Vue 组件中使用 listenunlisten

<template>
  <div>
    当前路由: {{ currentRoute }}
  </div>
</template>

<script>
import { useRoute, useRouter } from 'vue-router';
import { ref, onMounted, onUnmounted } from 'vue';

export default {
  setup() {
    const route = useRoute();
    const router = useRouter();
    const currentRoute = ref(route.path);

    let unlisten = null; // 保存 unlisten 函数

    onMounted(() => {
      // 注册监听器
      unlisten = router.history.listen((to, from) => {
        console.log('路由从', from.path, '变化到', to.path);
        currentRoute.value = to.path;
      });
    });

    onUnmounted(() => {
      // 组件卸载时,取消监听
      if (unlisten) {
        unlisten();
        unlisten = null; // 避免重复调用
      }
    });

    return {
      currentRoute,
    };
  },
};
</script>

在这个例子中,我们在组件的 onMounted 钩子函数中注册了 listen 监听器,并在 onUnmounted 钩子函数中调用 unlisten 函数来取消监听。 这样可以确保组件在卸载时,不会再收到路由变化的通知,从而避免潜在的错误和内存泄漏。

进阶:state 参数

listen 函数的回调函数会接收两个参数:locationstatelocation 包含了新的 URL 信息,state 包含了与该 URL 相关联的状态数据。

state 数据可以通过 history.pushStatehistory.replaceState 方法来设置。

// 设置 state 数据
history.pushState({ data: 'some data' }, '', '/about');

// 在 listen 回调函数中,可以访问 state 数据
router.history.listen((to, from, state) => {
  console.log('state 数据:', state); // { data: 'some data' }
});

state 数据可以用于存储一些与路由相关的临时数据,比如滚动位置、表单数据等等。 当用户点击浏览器的前进/后退按钮时,这些数据也会被恢复,从而提供更好的用户体验。

注意事项

  • 及时取消监听: 在组件卸载或者不再需要监听路由变化时,一定要及时调用 unlisten 函数,防止内存泄漏。
  • 避免重复注册监听器: 确保只注册一次监听器,避免多次执行相同的回调函数。
  • Hash ModelistenHash Mode 下,Vue Router 监听的是 hashchange 事件,而不是 popstate 事件。 listenunlisten 的机制是类似的,只是事件类型不同。

总结

listenunlisten 是 Vue Router 源码中非常重要的两个函数。 它们实现了路由监听的功能,使得 Vue Router 能够感知到浏览器的历史记录变化,并执行相应的操作。

通过理解 listenunlisten 的机制,我们可以更好地理解 Vue Router 的内部工作原理,并能够更灵活地使用 Vue Router 来构建复杂的单页应用。

好了,今天的讲座就到这里。 希望大家有所收获! 记住,写代码要细心,要记得及时清理垃圾 (取消监听),这样才能写出高性能、高质量的代码!

有问题欢迎提问! 如果没有问题,那就…下课!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注