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

各位观众老爷,大家好!今天咱们来聊聊 Vue Router 里一个非常重要的角色—— history 对象,特别是它的 listenunlisten 方法。这俩兄弟,看似简单,实则肩负重任,直接关系到你的 Vue 应用如何与浏览器的历史记录愉快地玩耍。

开场白:历史的轮子与 Vue Router 的故事

话说,咱们在浏览器里冲浪的时候,前进后退按钮那可是神器。每次点击,浏览器都会在它的历史记录里翻来覆去,带你回到过去,或者走向未来。而 Vue Router,作为前端路由的扛把子,自然也要跟这历史记录打交道。history 对象,就是 Vue Router 操纵历史记录的利器。

history 对象,不是你随便捏泥巴捏出来的,它其实是浏览器提供的 History API 的封装。这 API 允许我们以编程的方式访问和操作浏览器的历史栈,而无需真的重新加载页面。

第一幕:history 对象的身世之谜

在 Vue Router 中,history 对象主要有三种类型:

  1. HTML5History (也叫 createWebHistory): 这是最推荐的模式,利用了 pushStatereplaceState 这两个猛将,可以在不刷新页面的情况下改变 URL,并且可以记录到浏览器的历史栈中。
  2. HashHistory (也叫 createWebHashHistory): 这个老古董,利用 URL 中的 hash 部分(# 符号后面的内容)来模拟路由。好处是兼容性好,坏处是 URL 看起来不太优雅,搜索引擎也不太喜欢。
  3. MemoryHistory (也叫 createMemoryHistory): 这个家伙不会真的跟浏览器历史记录打交道,而是把路由状态保存在内存里。适合 SSR (服务端渲染) 和测试环境。

今天咱们主要聚焦 HTML5History,毕竟它是主流,也是 Vue Router 官方推荐的。

第二幕:listen 大法的修炼

listen 方法,顾名思义,就是“监听”。它允许你注册一个回调函数,当路由发生变化时,这个回调函数就会被触发。这个“路由变化”,指的不仅仅是 router.pushrouter.replace 引起的,也包括用户点击浏览器的前进后退按钮。

咱们先来看看 HTML5History 里的 listen 方法是怎么实现的 (简化版):

class HTML5History {
  constructor(router, base) {
    this.router = router;
    this.base = base;
    this.listeners = []; // 用于存放监听函数
    this.current = { path: '/', fullPath: '/', query: {}, params: {} }; // 当前路由状态
  }

  listen(callback) {
    this.listeners.push(callback);
    return () => { // 返回一个 unlisten 函数
      this.listeners = this.listeners.filter(listener => listener !== callback);
    };
  }

  // 内部方法,用于通知所有监听者
  _onTransition(to, from, trigger) {
    this.listeners.forEach(listener => {
      listener(to, from, trigger);
    });
  }

  push(to) {
    // ... 省略复杂的路由跳转逻辑
    window.history.pushState({ key: Date.now() }, '', to); // 使用 pushState 修改 URL
    const next = this.router.resolve(to);
    const from = this.current;
    this.current = next;
    this._onTransition(next, from, NavigationType.push); // 通知监听者
  }

  replace(to) {
    // ... 省略复杂的路由跳转逻辑
    window.history.replaceState({ key: Date.now() }, '', to); // 使用 replaceState 修改 URL
    const next = this.router.resolve(to);
    const from = this.current;
    this.current = next;
    this._onTransition(next, from, NavigationType.replace); // 通知监听者
  }
}

const NavigationType = {
  push: 'push',
  replace: 'replace',
  pop: 'pop',
};

这段代码的关键点:

  • this.listeners: 这是一个数组,用于存储所有注册的监听函数。
  • listen(callback): 这个方法把传入的回调函数 callback 添加到 this.listeners 数组中。
  • return () => { ... }: listen 方法返回一个函数,这个函数的作用是“取消监听”,也就是从 this.listeners 数组中移除对应的回调函数。
  • _onTransition(to, from, trigger): 这是一个内部方法,当路由发生变化时,它会遍历 this.listeners 数组,依次调用所有的监听函数。
  • push(to)replace(to): 这两个方法负责修改 URL,并调用 _onTransition 通知监听者。

举个栗子:

const router = new VueRouter({
  history: createWebHistory(),
  routes: [
    { path: '/', component: { template: '<div>Home</div>' } },
    { path: '/about', component: { template: '<div>About</div>' } },
  ],
});

const unlisten = router.history.listen((to, from) => {
  console.log(`路由从 ${from.path} 变成了 ${to.path}`);
});

router.push('/about'); // 控制台输出:路由从 / 变成了 /about

unlisten(); // 取消监听

router.push('/'); // 控制台不再输出任何内容

这个例子演示了如何使用 listen 方法注册一个监听函数,并在路由变化时得到通知。同时,也演示了如何使用 unlisten 函数取消监听。

第三幕:unlisten 剑法的精髓

unlisten 方法,是 listen 方法返回的那个函数。它的作用是“取消监听”,也就是从 this.listeners 数组中移除对应的回调函数。

为什么要取消监听呢?

  • 避免内存泄漏: 如果你注册的监听函数引用了组件实例或其他较大的对象,而你又没有及时取消监听,那么这些对象可能无法被垃圾回收,导致内存泄漏。
  • 避免重复执行: 在某些情况下,你可能只需要监听一段时间,或者只需要在特定的组件中监听。如果忘记取消监听,可能会导致回调函数被多次执行,产生意想不到的 bug。

unlisten 的实现非常简单,就是从 this.listeners 数组中移除对应的回调函数:

// 在上面的 HTML5History 类中
listen(callback) {
  this.listeners.push(callback);
  return () => { // 返回一个 unlisten 函数
    this.listeners = this.listeners.filter(listener => listener !== callback);
  };
}

注意事项:

  • unlisten 函数只能取消同一个回调函数的监听。如果你注册了多个相同的回调函数,unlisten 只会取消其中一个。
  • 一定要在组件销毁之前取消监听,避免内存泄漏。通常在 beforeUnmountunmounted 钩子函数中调用 unlisten

第四幕:与浏览器历史栈的爱恨情仇

HTML5History 能够与浏览器历史栈进行交互,主要依靠的是 pushStatereplaceState 这两个 API。

  • pushState(state, title, url): 这个方法会在浏览器的历史栈中添加一个新的条目。state 是一个与新条目关联的 JavaScript 对象,title 是页面的标题 (通常可以为空字符串),url 是新的 URL。
  • replaceState(state, title, url): 这个方法会替换浏览器历史栈中的当前条目。参数和 pushState 相同。

Vue Router 在调用 pushreplace 方法时,会分别调用 pushStatereplaceState 来修改 URL,并更新浏览器的历史栈。

但是,问题来了:用户点击浏览器的前进后退按钮时,浏览器并不会主动通知 Vue Router。Vue Router 需要监听 popstate 事件,才能知道用户点击了前进后退按钮。

class HTML5History {
  constructor(router, base) {
    // ... 省略之前的代码

    // 监听 popstate 事件
    this.cleanup = () => {
      window.removeEventListener('popstate', this.popStateHandler);
    };
    this.popStateHandler = ({ state }) => {
      if (state) {
        // ... 省略复杂的路由跳转逻辑,包括解析 URL,匹配路由等
        const next = this.router.resolve(window.location.pathname);
        const from = this.current;
        this.current = next;
        this._onTransition(next, from, NavigationType.pop); // 通知监听者
      } else {
        //  state 为 null,说明是初始化时触发的 popstate 事件,忽略
      }
    };

    window.addEventListener('popstate', this.popStateHandler);
  }

  destroy() {
    this.cleanup();
  }
}

这段代码的关键点:

  • popstate 事件: 当用户点击浏览器的前进后退按钮时,会触发 popstate 事件。
  • popStateHandler: 这个函数是 popstate 事件的处理函数。它会解析 URL,匹配路由,并调用 _onTransition 通知监听者。
  • state: popstate 事件对象有一个 state 属性,它包含了调用 pushStatereplaceState 时传入的 state 对象。Vue Router 利用这个 state 对象来判断是否需要处理 popstate 事件。

表格总结:

方法/事件 作用 触发时机
pushState 在浏览器历史栈中添加新的条目 调用 router.push
replaceState 替换浏览器历史栈中的当前条目 调用 router.replace
popstate 当用户点击浏览器的前进后退按钮时触发 用户点击浏览器的前进后退按钮
listen 注册一个回调函数,当路由发生变化时被调用 调用 router.pushrouter.replace 或用户点击浏览器的前进后退按钮导致路由变化时
unlisten 取消监听,移除之前注册的回调函数 组件销毁之前,避免内存泄漏

第五幕:实战演练

咱们来做一个简单的 demo,演示如何使用 listenunlisten 来实现一个“路由守卫”的功能。

<template>
  <div>
    <router-link to="/">Home</router-link> |
    <router-link to="/admin">Admin</router-link>
    <router-view />
  </div>
</template>

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

export default {
  setup() {
    const router = useRouter();
    const route = useRoute();
    const isLoggedIn = ref(false); // 模拟用户登录状态
    let unlisten = null;

    onMounted(() => {
      unlisten = router.history.listen((to, from) => {
        if (to.path === '/admin' && !isLoggedIn.value) {
          console.log('没有权限访问 Admin 页面,跳转到首页');
          router.push('/');
        }
      });
    });

    onBeforeUnmount(() => {
      if (unlisten) {
        unlisten(); // 组件销毁之前取消监听
      }
    });

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

这个例子中,我们在 onMounted 钩子函数中注册了一个监听函数。当路由切换到 /admin 页面时,如果用户没有登录,就会被重定向到首页。在 onBeforeUnmount 钩子函数中,我们取消了监听,避免内存泄漏。

剧终:总结与展望

今天咱们深入剖析了 Vue Router 中 history 对象的 listenunlisten 机制,以及它们如何与浏览器历史栈进行交互。希望通过今天的讲解,大家能够对 Vue Router 的内部原理有更深入的了解,从而更好地使用 Vue Router 构建你的 Vue 应用。

记住,listenunlisten 是两把利剑,用好了可以让你在路由的世界里游刃有余,用不好可能会让你陷入内存泄漏的泥潭。

好了,今天的讲座就到这里。感谢大家的观看,我们下期再见!

发表回复

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