从"坐电梯"到"前端路由"——深入理解 Hash 路由原理

从"坐电梯"到"前端路由"------深入理解 Hash 路由原理

一、浏览器是怎么工作的?

在聊路由之前,我们先搞清楚一个最基础的问题:在浏览器里输入一个 URL,到底发生了什么?

你的笔记里把整个过程拆成了 5 个步骤,这个拆解非常精准:

arduino 复制代码
URL → 浏览器发起请求 → 服务器响应 text/html → 浏览器渲染页面 → 插入一条浏览历史

每一步的底层含义是这样的:

  1. URL 是浏览器的访问代理 :你在地址栏输入的东西,本质上是一个"资源定位符",告诉浏览器"我要找什么东西"。浏览器自己不存储网页,它只是一个代理,替你去服务器那里拿数据。
  2. HTTP 协议发起请求:浏览器按照 HTTP 协议的格式,向服务器发送一个请求报文。这个报文里包含了方法(GET/POST)、路径(/index.html)、头信息等。
  3. 服务器伺服并响应 :服务器收到请求后,根据路径找到对应的资源,然后返回一个 HTTP 响应。响应头里 Content-Type: text/html 告诉浏览器:"我返回的是一个 HTML 文档,请用 HTML 的方式解析我。"
  4. 浏览器渲染页面 :浏览器拿到 HTML 之后,开始构建 DOM 树、CSSOM 树,合并成渲染树,然后进行布局(Layout)和绘制(Paint),最终把像素显示在屏幕上。这个过程叫关键渲染路径(Critical Rendering Path)。
  5. 插入浏览历史 :渲染完成后,浏览器会在当前标签页的 history 栈中插入一条记录。navigator 对象就是用来访问这些历史记录的。这一步非常关键------没有它,"前进/后退"功能就不存在。
javascript 复制代码
// navigator 对象承载了浏览历史相关的所有信息
console.log(window.history.length); // 当前标签页的历史记录条数

你笔记里提到的 navigator 对象,实际上浏览历史的核心接口在 window.history 上,它是浏览器提供给 JavaScript 操作历史栈的唯一入口。


二、传统多页面模式:demo 文件夹解读

打开 demo/index.htmldemo/about.html,你会发现这是最"原始"的网页跳转方式:

xml 复制代码
<!-- demo/index.html -->
<li><a href="http://127.0.0.1:5500/djz-ai/fe/history/demo/index.html">首页</a></li>
<li><a href="http://127.0.0.1:5500/djz-ai/fe/history/demo/about.html">关于我们</a></li>

这种方式的特点

每个 <a> 标签的 href 指向的是一个完整的、独立的 HTML 文件。当你点击"关于我们"时:

  1. 浏览器向服务器发起一个全新的 HTTP 请求:GET /about.html
  2. 服务器返回 about.html 的完整内容
  3. 浏览器销毁当前页面的所有状态(DOM、JS 变量、事件监听器全部清空)
  4. 重新解析、重新渲染整个页面
  5. 屏幕会出现短暂的白屏(网速越慢越明显)

你的笔记精准地概括了这个问题:

"传统多页面每次都需要重新渲染,移动端时代有点没必要了,页面会白一下。"

这里涉及一个深层原因:浏览器的导航是"破坏性"的。当一个文档被卸载(unload),它对应的 JavaScript 执行上下文会被完全销毁。这意味着:

  • 所有内存中的变量、闭包、定时器 → 消失
  • 所有事件监听器 → 解绑
  • DOM 树 → 从内存中移除
  • CSSOM → 重建

这不是"优化不够好",而是浏览器架构层面的设计选择------新页面的 JS 执行环境必须是干净隔离的,这是安全模型的要求。


三、锚点链接:Hash 的原始使命

在学习 Hash 路由之前,必须先理解 hash 最初是干什么的。打开 demo2/demo.html

css 复制代码
<a name="top"></a>
<a href="#bottom">去到底部</a>
<div style="height:200vh;background-color:yellow;"></div>
<a href="#top">回到顶部</a>
<div style="height:300vh;background-color:red;"></div>
<a name="bottom"></a>

锚点的底层机制

<a name="top"> 在页面中埋下一个命名锚点 (named anchor),href="#bottom" 告诉浏览器:"跳转到当前页面中 name="bottom" 的元素位置"。

当 URL 的 hash 部分改变时------比如 page.html#toppage.html#bottom------浏览器做了两件事:

  1. 不发起新的网络请求 。因为 # 后面的内容根本不会发送到服务器,它是纯客户端的概念。
  2. 滚动到对应锚点位置 。浏览器在 DOM 树中查找 name="bottom"id="bottom" 的元素,然后调用滚动 API 将视口定位到该元素。

hashchange 事件

demo2/demo.html 里监听了这个事件:

javascript 复制代码
window.addEventListener('hashchange', function (event) {
  console.log('hash改变了');
  console.log(event.newURL); // hash 改变后的完整 URL
  console.log(event.oldURL); // hash 改变前的完整 URL
});

hashchange 是浏览器原生提供的事件。当 hash 从 #top 变为 #bottom 时:

  • event.newURL = http://...demo.html#bottom
  • event.oldURL = http://...demo.html#top

关键洞察:hash 改变时,页面不会重新加载,不会发请求,DOM 状态保持完整,JS 执行上下文不销毁。 这是 hash 能做前端路由的根本原因。


四、SPA 的核心矛盾与解决思路

矛盾在哪?

你的笔记里写得很清楚:

"url 一定要变,不同的 url 对应不同的资源。但 url 变了又要页面不刷新。"

这是一个看似矛盾的诉求

  • 从用户体验角度:URL 必须变(用户要能收藏、分享、前进后退)
  • 从性能角度:页面不能刷新(要避免白屏,保持 JS 状态)

传统多页面模式解决了前者(URL 和资源一一对应),但牺牲了后者(每次都要重新加载)。

hash 如何破局?

"改变 hash,url 改变了,不会跳转。"

这句话是整篇文章的核心。我们来看 URL 的结构:

ruby 复制代码
http(s)://www.baidu.com/u/123?a=1&b=2#page1
 协议      主机名       路径    查询参数    hash

#page1 这部分有几个关键特性:

特性 说明
不会发送到服务器 HTTP 请求中不包含 # 之后的内容
改变时不刷新页面 浏览器不会发起导航
会产生历史记录 浏览器的前进/后退按钮可用
可被 JS 监听 hashchange 事件能捕获变化

这四点组合在一起,刚好满足了 SPA 的全部需求:

  • URL 可以变化(不同 hash 对应不同"页面")→ ✅ 可收藏、可分享
  • 不会重新加载页面 → ✅ 无白屏、状态保留
  • 有历史记录 → ✅ 前进后退正常
  • JS 可以监听变化 → ✅ 根据 hash 动态渲染内容

SPA 的本质

"单页应用 Single Page Application,怎么把丰富的内容在一个网页里显示?DOM 编程。"

SPA 的本质是在单个 HTML 文档内,通过 JavaScript 动态替换 DOM 内容来模拟"页面切换" 。前端路由就是这套机制的"指挥官"------它监听 URL 变化,然后决定显示什么内容。URL 是"地址",DOM 是"房间里的家具",前端路由是"搬家具的人"。


五、手写 HashRouter:demo2/index.html 逐行解析

这是文章的重头戏。demo2/index.html 实现了一个最小可用的 Hash 路由器。我们逐段拆解。

5.1 页面结构

xml 复制代码
<nav>
  <ul>
    <li><a href="#/page1">页面一</a></li>
    <li><a href="#/page2">页面二</a></li>
    <li><a href="#/page3">页面三</a></li>
  </ul>
</nav>
<div id="container"></div>

这里的关键变化:href 不再是完整的 URL(如 http://...about.html),而是 #/page1

这意味着点击链接时,浏览器不会发起 HTTP 请求,只会改变 URL 的 hash 部分。#/page1 中的 /page1 其实是一个约定 ------它模仿了服务器路径的写法(/page1),让 URL 看起来像"正经的路径",但本质上它还是 hash 的一部分。

<div id="container">挂载点,SPA 中所有"页面"的内容都渲染到这里。

5.2 HashRouter 类

kotlin 复制代码
class HashRouter {
  constructor() {
    this.routers = {}; // 前端路由集合
    window.addEventListener('hashchange', this.load.bind(this));
  }

this.routers = {}

这是一个对象,用来存储 hash 路径 → 回调函数 的映射关系。比如:

ini 复制代码
{
  '/page1': () => container.innerHTML = '页面一',
  '/page2': () => container.innerHTML = '页面二',
  '/page3': () => container.innerHTML = '页面三'
}

这个对象就是"路由表"------前端路由的核心数据结构。它回答了"当 URL 是这个 hash 时,我应该渲染什么内容"。

this.load.bind(this) 深度解析

这是整个代码中最能体现 JavaScript 底层机制的一行。我们先看如果bind 会发生什么:

javascript 复制代码
window.addEventListener('hashchange', this.load);
// 当 hashchange 触发时:
load() {
  console.log(this); // → window(不是 HashRouter 实例!)
  let hash = location.hash.slice(1);
  let handler = this.routers[hash]; // this.routers 是 undefined!
}

为什么会这样?因为 addEventListener 的回调函数中,this 默认指向事件绑定的 DOM 元素 (这里是 window)。

你代码里的注释写得很到位:

// this 指向事件发生的对象 window

这涉及到 JavaScript 中 this 的绑定规则之一:当函数作为事件处理函数被调用时,this 指向绑定事件的元素。

三种解决方案:

方法 写法 原理 是否立即执行
bind this.load.bind(this) 返回一个 this 被永久锁定的新函数 ❌ 不立即执行
call --- 立即调用函数,第一个参数指定 this ✅ 立即执行
apply --- call,但参数以数组形式传递 ✅ 立即执行

这里必须用 bind 而不是 call/apply,因为 addEventListener 的第二个参数需要的是一个函数引用 ,而不是函数的执行结果。call/apply 会立即调用函数,而我们只需要传递函数,等事件触发时才执行。

从更底层的角度看,bind 的实现原理可以简化理解为:

javascript 复制代码
Function.prototype.myBind = function (context) {
  const fn = this; // 保存原函数
  return function (...args) {
    return fn.apply(context, args); // 用 apply 强制指定 this
  };
};

所以 this.load.bind(this) 本质上是创建了一个闭包,在闭包内部用 applythis 强行指定为构造函数的实例对象。

5.3 register 方法

javascript 复制代码
register(hash, callback) {
  this.routers[hash] = callback;
}

这个方法简单但关键。它接收两个参数:

  • hash:hash 路径(如 '/page1'
  • callback:当匹配到这个 hash 时要执行的回调函数

每调用一次 register,就往 this.routers 对象里添加一条记录。这是策略模式的体现------把"做什么"(callback)和"什么时候做"(hash 匹配)分离开。

5.4 load 方法

javascript 复制代码
load() {
  console.log(this); // HashRouter 实例(bind 确保的)
  let hash = location.hash.slice(1); // 去掉开头的 #
  let handler;
  if (!hash) {
    // hash 为空时显示默认内容
  } else {
    handler = this.routers[hash]; // 从路由表中查找
  }
  handler.call(this); // 执行回调,this 依然指向实例
}

逐行分析:

location.hash.slice(1)

location.hash 获取的是 #/page1(带 # 号),slice(1) 切掉第一个字符得到 /page1。这样就和 register 时存的 key 对上了。

handler = this.routers[hash]

从路由表对象中取值。这是一个 O(1) 的操作,因为 JavaScript 对象的属性查找本质上是哈希表查找。

handler.call(this)

为什么不用 handler() 直接调用?用 call(this) 是为了让回调函数内部的 this 也指向 HashRouter 实例,保持一致性。如果回调里需要访问 this.routers 或实例上的其他属性,这个 this 就能用。

5.5 组装使用

ini 复制代码
let router = new HashRouter();
let container = document.getElementById('container');

router.register('/page1', () => container.innerHTML = '页面一');
router.register('/page2', () => container.innerHTML = '页面二');
router.register('/page3', () => container.innerHTML = '页面三');

这里的回调函数里直接操作 container.innerHTML,因为 container 在闭包中被捕获了。实际项目中这里通常是渲染组件。

5.6 完整数据流

python 复制代码
用户点击 #/page1
  → URL hash 变为 #/page1
  → 浏览器触发 hashchange 事件
  → load() 被调用(this 指向 router 实例)
  → location.hash.slice(1) → '/page1'
  → this.routers['/page1'] → 回调函数
  → 回调执行 → container.innerHTML = '页面一'
  → DOM 更新,用户看到新内容
  → 全程无 HTTP 请求,无页面刷新

六、从 demo 到 demo2:一次架构演进

把两个文件夹放在一起对比,能看到一条清晰的演进路线:

维度 demo(传统多页) demo2(SPA + Hash 路由)
跳转方式 完整 URL,<a href="http://..."> Hash 片段,<a href="#/page1">
请求次数 每次跳转都发 HTTP 请求 0 次额外请求
页面刷新 整页刷新,有白屏 无刷新,无白屏
JS 状态 每次跳转后销毁重建 全程保留
路由控制 服务器决定 JavaScript 控制(前端路由)
URL 结构 /index.html/about.html #/page1#/page2

这个演进不是简单的"技术升级",而是职责转移------路由的职责从服务器端转移到了浏览器端。服务器不再关心"用户当前在哪个页面",它只负责提供数据和初始 HTML,页面之间的导航完全由前端控制。

这也是你笔记里说的:

"前后端分离的,前端也要独立的路由。"


七、总结

回顾整个知识体系的递进关系:

  1. 浏览器导航的底层机制:URL → 请求 → 响应 → 渲染 → 历史记录
  2. 传统多页面的痛点:每次跳转 = 销毁一切 + 重新加载 + 白屏
  3. 锚点链接揭示了 hash 的特性:改变 hash 不刷新页面,但有历史记录,且可被 JS 监听
  4. SPA 利用这个特性:用 hash 变化模拟"页面切换",用 DOM 操作替换内容
  5. HashRouter 是最小实现 :路由表(对象映射)+ hashchange 事件 + bind 绑定 this

Hash 路由是前端路由的"第一代方案",之后还有 History API(pushState/popState)实现的更优雅的方案,但理解了 hash 路由,就理解了前端路由最核心的思想:用 JavaScript 接管浏览器的导航行为,在不刷新页面的前提下实现 URL 与视图的同步。# 从"坐电梯"到"前端路由"------深入理解 Hash 路由原理

一、浏览器是怎么工作的?

在聊路由之前,我们先搞清楚一个最基础的问题:在浏览器里输入一个 URL,到底发生了什么?

你的笔记里把整个过程拆成了 5 个步骤,这个拆解非常精准:

arduino 复制代码
URL → 浏览器发起请求 → 服务器响应 text/html → 浏览器渲染页面 → 插入一条浏览历史

每一步的底层含义是这样的:

  1. URL 是浏览器的访问代理 :你在地址栏输入的东西,本质上是一个"资源定位符",告诉浏览器"我要找什么东西"。浏览器自己不存储网页,它只是一个代理,替你去服务器那里拿数据。
  2. HTTP 协议发起请求:浏览器按照 HTTP 协议的格式,向服务器发送一个请求报文。这个报文里包含了方法(GET/POST)、路径(/index.html)、头信息等。
  3. 服务器伺服并响应 :服务器收到请求后,根据路径找到对应的资源,然后返回一个 HTTP 响应。响应头里 Content-Type: text/html 告诉浏览器:"我返回的是一个 HTML 文档,请用 HTML 的方式解析我。"
  4. 浏览器渲染页面 :浏览器拿到 HTML 之后,开始构建 DOM 树、CSSOM 树,合并成渲染树,然后进行布局(Layout)和绘制(Paint),最终把像素显示在屏幕上。这个过程叫关键渲染路径(Critical Rendering Path)。
  5. 插入浏览历史 :渲染完成后,浏览器会在当前标签页的 history 栈中插入一条记录。navigator 对象就是用来访问这些历史记录的。这一步非常关键------没有它,"前进/后退"功能就不存在。
javascript 复制代码
// navigator 对象承载了浏览历史相关的所有信息
console.log(window.history.length); // 当前标签页的历史记录条数

你笔记里提到的 navigator 对象,实际上浏览历史的核心接口在 window.history 上,它是浏览器提供给 JavaScript 操作历史栈的唯一入口。


二、传统多页面模式:demo 文件夹解读

打开 demo/index.htmldemo/about.html,你会发现这是最"原始"的网页跳转方式:

xml 复制代码
<!-- demo/index.html -->
<li><a href="http://127.0.0.1:5500/djz-ai/fe/history/demo/index.html">首页</a></li>
<li><a href="http://127.0.0.1:5500/djz-ai/fe/history/demo/about.html">关于我们</a></li>

这种方式的特点

每个 <a> 标签的 href 指向的是一个完整的、独立的 HTML 文件。当你点击"关于我们"时:

  1. 浏览器向服务器发起一个全新的 HTTP 请求:GET /about.html
  2. 服务器返回 about.html 的完整内容
  3. 浏览器销毁当前页面的所有状态(DOM、JS 变量、事件监听器全部清空)
  4. 重新解析、重新渲染整个页面
  5. 屏幕会出现短暂的白屏(网速越慢越明显)

你的笔记精准地概括了这个问题:

"传统多页面每次都需要重新渲染,移动端时代有点没必要了,页面会白一下。"

这里涉及一个深层原因:浏览器的导航是"破坏性"的。当一个文档被卸载(unload),它对应的 JavaScript 执行上下文会被完全销毁。这意味着:

  • 所有内存中的变量、闭包、定时器 → 消失
  • 所有事件监听器 → 解绑
  • DOM 树 → 从内存中移除
  • CSSOM → 重建

这不是"优化不够好",而是浏览器架构层面的设计选择------新页面的 JS 执行环境必须是干净隔离的,这是安全模型的要求。


三、锚点链接:Hash 的原始使命

在学习 Hash 路由之前,必须先理解 hash 最初是干什么的。打开 demo2/demo.html

css 复制代码
<a name="top"></a>
<a href="#bottom">去到底部</a>
<div style="height:200vh;background-color:yellow;"></div>
<a href="#top">回到顶部</a>
<div style="height:300vh;background-color:red;"></div>
<a name="bottom"></a>

锚点的底层机制

<a name="top"> 在页面中埋下一个命名锚点 (named anchor),href="#bottom" 告诉浏览器:"跳转到当前页面中 name="bottom" 的元素位置"。

当 URL 的 hash 部分改变时------比如 page.html#toppage.html#bottom------浏览器做了两件事:

  1. 不发起新的网络请求 。因为 # 后面的内容根本不会发送到服务器,它是纯客户端的概念。
  2. 滚动到对应锚点位置 。浏览器在 DOM 树中查找 name="bottom"id="bottom" 的元素,然后调用滚动 API 将视口定位到该元素。

hashchange 事件

demo2/demo.html 里监听了这个事件:

javascript 复制代码
window.addEventListener('hashchange', function (event) {
  console.log('hash改变了');
  console.log(event.newURL); // hash 改变后的完整 URL
  console.log(event.oldURL); // hash 改变前的完整 URL
});

hashchange 是浏览器原生提供的事件。当 hash 从 #top 变为 #bottom 时:

  • event.newURL = http://...demo.html#bottom
  • event.oldURL = http://...demo.html#top

关键洞察:hash 改变时,页面不会重新加载,不会发请求,DOM 状态保持完整,JS 执行上下文不销毁。 这是 hash 能做前端路由的根本原因。


四、SPA 的核心矛盾与解决思路

矛盾在哪?

你的笔记里写得很清楚:

"url 一定要变,不同的 url 对应不同的资源。但 url 变了又要页面不刷新。"

这是一个看似矛盾的诉求

  • 从用户体验角度:URL 必须变(用户要能收藏、分享、前进后退)
  • 从性能角度:页面不能刷新(要避免白屏,保持 JS 状态)

传统多页面模式解决了前者(URL 和资源一一对应),但牺牲了后者(每次都要重新加载)。

hash 如何破局?

"改变 hash,url 改变了,不会跳转。"

这句话是整篇文章的核心。我们来看 URL 的结构:

ruby 复制代码
http(s)://www.baidu.com/u/123?a=1&b=2#page1
 协议      主机名       路径    查询参数    hash

#page1 这部分有几个关键特性:

特性 说明
不会发送到服务器 HTTP 请求中不包含 # 之后的内容
改变时不刷新页面 浏览器不会发起导航
会产生历史记录 浏览器的前进/后退按钮可用
可被 JS 监听 hashchange 事件能捕获变化

这四点组合在一起,刚好满足了 SPA 的全部需求:

  • URL 可以变化(不同 hash 对应不同"页面")→ ✅ 可收藏、可分享
  • 不会重新加载页面 → ✅ 无白屏、状态保留
  • 有历史记录 → ✅ 前进后退正常
  • JS 可以监听变化 → ✅ 根据 hash 动态渲染内容

SPA 的本质

"单页应用 Single Page Application,怎么把丰富的内容在一个网页里显示?DOM 编程。"

SPA 的本质是在单个 HTML 文档内,通过 JavaScript 动态替换 DOM 内容来模拟"页面切换" 。前端路由就是这套机制的"指挥官"------它监听 URL 变化,然后决定显示什么内容。URL 是"地址",DOM 是"房间里的家具",前端路由是"搬家具的人"。


五、手写 HashRouter:demo2/index.html 逐行解析

这是文章的重头戏。demo2/index.html 实现了一个最小可用的 Hash 路由器。我们逐段拆解。

5.1 页面结构

xml 复制代码
<nav>
  <ul>
    <li><a href="#/page1">页面一</a></li>
    <li><a href="#/page2">页面二</a></li>
    <li><a href="#/page3">页面三</a></li>
  </ul>
</nav>
<div id="container"></div>

这里的关键变化:href 不再是完整的 URL(如 http://...about.html),而是 #/page1

这意味着点击链接时,浏览器不会发起 HTTP 请求,只会改变 URL 的 hash 部分。#/page1 中的 /page1 其实是一个约定 ------它模仿了服务器路径的写法(/page1),让 URL 看起来像"正经的路径",但本质上它还是 hash 的一部分。

<div id="container">挂载点,SPA 中所有"页面"的内容都渲染到这里。

5.2 HashRouter 类

kotlin 复制代码
class HashRouter {
  constructor() {
    this.routers = {}; // 前端路由集合
    window.addEventListener('hashchange', this.load.bind(this));
  }

this.routers = {}

这是一个对象,用来存储 hash 路径 → 回调函数 的映射关系。比如:

ini 复制代码
{
  '/page1': () => container.innerHTML = '页面一',
  '/page2': () => container.innerHTML = '页面二',
  '/page3': () => container.innerHTML = '页面三'
}

这个对象就是"路由表"------前端路由的核心数据结构。它回答了"当 URL 是这个 hash 时,我应该渲染什么内容"。

this.load.bind(this) 深度解析

这是整个代码中最能体现 JavaScript 底层机制的一行。我们先看如果bind 会发生什么:

javascript 复制代码
window.addEventListener('hashchange', this.load);
// 当 hashchange 触发时:
load() {
  console.log(this); // → window(不是 HashRouter 实例!)
  let hash = location.hash.slice(1);
  let handler = this.routers[hash]; // this.routers 是 undefined!
}

为什么会这样?因为 addEventListener 的回调函数中,this 默认指向事件绑定的 DOM 元素 (这里是 window)。

你代码里的注释写得很到位:

// this 指向事件发生的对象 window

这涉及到 JavaScript 中 this 的绑定规则之一:当函数作为事件处理函数被调用时,this 指向绑定事件的元素。

三种解决方案:

方法 写法 原理 是否立即执行
bind this.load.bind(this) 返回一个 this 被永久锁定的新函数 ❌ 不立即执行
call --- 立即调用函数,第一个参数指定 this ✅ 立即执行
apply --- call,但参数以数组形式传递 ✅ 立即执行

这里必须用 bind 而不是 call/apply,因为 addEventListener 的第二个参数需要的是一个函数引用 ,而不是函数的执行结果。call/apply 会立即调用函数,而我们只需要传递函数,等事件触发时才执行。

从更底层的角度看,bind 的实现原理可以简化理解为:

javascript 复制代码
Function.prototype.myBind = function (context) {
  const fn = this; // 保存原函数
  return function (...args) {
    return fn.apply(context, args); // 用 apply 强制指定 this
  };
};

所以 this.load.bind(this) 本质上是创建了一个闭包,在闭包内部用 applythis 强行指定为构造函数的实例对象。

5.3 register 方法

javascript 复制代码
register(hash, callback) {
  this.routers[hash] = callback;
}

这个方法简单但关键。它接收两个参数:

  • hash:hash 路径(如 '/page1'
  • callback:当匹配到这个 hash 时要执行的回调函数

每调用一次 register,就往 this.routers 对象里添加一条记录。这是策略模式的体现------把"做什么"(callback)和"什么时候做"(hash 匹配)分离开。

5.4 load 方法

javascript 复制代码
load() {
  console.log(this); // HashRouter 实例(bind 确保的)
  let hash = location.hash.slice(1); // 去掉开头的 #
  let handler;
  if (!hash) {
    // hash 为空时显示默认内容
  } else {
    handler = this.routers[hash]; // 从路由表中查找
  }
  handler.call(this); // 执行回调,this 依然指向实例
}

逐行分析:

location.hash.slice(1)

location.hash 获取的是 #/page1(带 # 号),slice(1) 切掉第一个字符得到 /page1。这样就和 register 时存的 key 对上了。

handler = this.routers[hash]

从路由表对象中取值。这是一个 O(1) 的操作,因为 JavaScript 对象的属性查找本质上是哈希表查找。

handler.call(this)

为什么不用 handler() 直接调用?用 call(this) 是为了让回调函数内部的 this 也指向 HashRouter 实例,保持一致性。如果回调里需要访问 this.routers 或实例上的其他属性,这个 this 就能用。

5.5 组装使用

ini 复制代码
let router = new HashRouter();
let container = document.getElementById('container');

router.register('/page1', () => container.innerHTML = '页面一');
router.register('/page2', () => container.innerHTML = '页面二');
router.register('/page3', () => container.innerHTML = '页面三');

这里的回调函数里直接操作 container.innerHTML,因为 container 在闭包中被捕获了。实际项目中这里通常是渲染组件。

5.6 完整数据流

python 复制代码
用户点击 #/page1
  → URL hash 变为 #/page1
  → 浏览器触发 hashchange 事件
  → load() 被调用(this 指向 router 实例)
  → location.hash.slice(1) → '/page1'
  → this.routers['/page1'] → 回调函数
  → 回调执行 → container.innerHTML = '页面一'
  → DOM 更新,用户看到新内容
  → 全程无 HTTP 请求,无页面刷新

六、从 demo 到 demo2:一次架构演进

把两个文件夹放在一起对比,能看到一条清晰的演进路线:

维度 demo(传统多页) demo2(SPA + Hash 路由)
跳转方式 完整 URL,<a href="http://..."> Hash 片段,<a href="#/page1">
请求次数 每次跳转都发 HTTP 请求 0 次额外请求
页面刷新 整页刷新,有白屏 无刷新,无白屏
JS 状态 每次跳转后销毁重建 全程保留
路由控制 服务器决定 JavaScript 控制(前端路由)
URL 结构 /index.html/about.html #/page1#/page2

这个演进不是简单的"技术升级",而是职责转移------路由的职责从服务器端转移到了浏览器端。服务器不再关心"用户当前在哪个页面",它只负责提供数据和初始 HTML,页面之间的导航完全由前端控制。

这也是你笔记里说的:

"前后端分离的,前端也要独立的路由。"


七、总结

回顾整个知识体系的递进关系:

  1. 浏览器导航的底层机制:URL → 请求 → 响应 → 渲染 → 历史记录
  2. 传统多页面的痛点:每次跳转 = 销毁一切 + 重新加载 + 白屏
  3. 锚点链接揭示了 hash 的特性:改变 hash 不刷新页面,但有历史记录,且可被 JS 监听
  4. SPA 利用这个特性:用 hash 变化模拟"页面切换",用 DOM 操作替换内容
  5. HashRouter 是最小实现 :路由表(对象映射)+ hashchange 事件 + bind 绑定 this

Hash 路由是前端路由的"第一代方案",之后还有 History API(pushState/popState)实现的更优雅的方案,但理解了 hash 路由,就理解了前端路由最核心的思想:用 JavaScript 接管浏览器的导航行为,在不刷新页面的前提下实现 URL 与视图的同步。

相关推荐
梦想CAD控件1 小时前
网页端CAD的图形选择、编辑与夹点操作教程
前端·javascript·node.js
妙码生花1 小时前
从 PHP 到 AI + Golang,程序员自救转型手记(五十二):管理员权限检查中间件,AI 随意放行预闯大祸
前端·后端·go
月月大王的3D日记2 小时前
Three.js 入门系列(8):六种光源全解析 —— 关灯了,开光!
前端·javascript
属于自己的天空2 小时前
每天省去重复 Prompt:我用 Skills 把 CRUD 生成标准化了
前端·后端
卡卡罗特AI2 小时前
GitHub怎么用?零基础,保姆级使用教程-上!建议收藏
前端·后端·github
自然 醒2 小时前
v-tooltip自定义指令封装
前端·javascript·vue.js
因_崔斯汀3 小时前
Three.js 3D 热力云图效果实现
前端·three.js
爱勇宝3 小时前
人到了一定年纪,才会看懂这些人性真相
前端·后端