详解JavaScript实现监听路由变化

作者:半夏

时间:2026-10-24

浏览:399

本文详解前端路由变化的两种实现方式:Hash模式与History模式。重点解析History API中pushState、replaceState与popstate事件的机制差异,指出原生API不监听状态修改的局限。通过引入订阅发布模式(Dep与Watch),重新封装history方法,实现了对pushState和replaceState的监听,从而完善单页面应用的路由状态管理。

文章目录

前端实现路由切换主要依赖两种机制,其核心优势在于能够在不刷新页面的前提下完成URL的变更。

  1. 第一种方式是基于Hash值的变化,通过监听window.onhashchange事件来响应路由变动。
  2. 第二种方式是利用History API进行页面加载操作。与Hash模式不同,History模式的实现更为复杂:浏览器的前进、后退操作(通过history.back()、history.forward()和history.go()方法)会主动触发popstate事件,但通过代码调用pushState或replaceState修改历史记录时,并不会自动触发popstate事件。

History API详解

本节主要介绍History API的相关机制。

pushState()方法

该方法接收三个参数:状态对象(state object)、标题(title,目前通常被忽略)以及可选的URL。以下是对这三个参数的详细解析:

状态对象(state object):这是一个JavaScript对象,用于创建新的历史记录条目。当用户导航至该新状态时,会触发popstate事件,且该事件的state属性包含该状态对象的副本。状态对象可以是任何可序列化的数据。需要注意的是,Firefox会将状态对象保存在用户磁盘上以便浏览器重启后恢复,因此规定序列化后的数据大小不得超过640k。若传递的数据超过此限制,pushState()将抛出异常。如需存储更大体积的数据,建议结合使用sessionStorage或localStorage。

标题(title):目前Firefox浏览器忽略此参数,但未来版本可能会启用。为确保兼容性,建议传入空字符串,或者为跳转状态提供一个简短的标题描述。

URL:该参数定义了新的历史记录URL。调用pushState()后,浏览器不会立即加载该URL,但在某些特定场景下(如用户重启浏览器)可能会尝试加载(注意:不会加载资源文件,但可通过监听URL变化来更新视图,这是单页面路由实现的关键)。新URL不必是绝对路径,若为相对路径,则相对于当前URL解析。此外,新URL必须与当前页面同源,否则pushState()会抛出异常。若省略此参数,默认使用当前URL。

从某种意义上讲,调用pushState()与设置window.location = “#foo”类似,两者均会在当前页面创建并激活新的历史记录。然而,pushState()具备以下显著优势:

新URL可以是与当前页面同源的任意URL,而Hash模式仅允许修改同一文档内的哈希值。若无需更改URL,则无需操作;相比之下,设置window.location = “#foo”仅在当前哈希值非#foo时才会创建新的历史记录。此外,pushState()允许将任意数据与新历史记录关联,而Hash模式需将所有相关数据编码为短字符串。最后,若浏览器未来支持标题功能,该数据可被利用(Hash模式则不支持)。

需注意,调用pushState()绝不会触发hashchange事件,即使新URL与旧URL仅哈希部分不同也是如此。

pushState()应用场景

利用History API可实现无刷新修改URL或其参数。


window.history.replaceState('', '', `${window.location.origin}${window.location.pathname}type=a`);

replaceState()方法

history.replaceState()的使用方式与pushState()高度相似,主要区别在于replaceState()用于修改当前的历史记录项,而非创建新的记录。需要注意的是,这并不会阻止在全局浏览器历史记录中创建一个新的历史条目。

popstate事件

通过window.onpopstate可以监听返回事件。


window.onpopstate = funcRef;

funcRef : (Event:{state:any})=>void

每当激活状态的历史记录发生改变时,就会触发popstate事件,该事件在window对象上触发(即window.onpopstate)。如果当前激活的历史记录条目是由history.pushState()创建,或由history.replaceState()修改,则popstate事件对象的state属性包含该历史记录条目状态对象的副本。

注意:

  1. 调用history.pushState()或history.replaceState()不会触发popstate事件。popstate事件仅在浏览器的特定行为下触发,例如点击前进/后退按钮(或在JavaScript中调用history.back()、history.forward()、history.go()方法)。此外,a标签的锚点跳转也会触发该事件。
  2. 各浏览器在页面加载时对popstate事件的触发行为存在差异:Chrome和Safari会触发popstate事件,而Firefox不会。

如何监听pushState和replaceState?

我们可以通过订阅-发布模式自行实现监听机制:首先利用Dep和Watch,这是参考Vue源码中Dep与Watcher之间交互方式实现的简化版本。


class Dep {                  // 订阅池
    constructor(name){
        this.id = new Date() //这里简单的运用时间戳做订阅池的ID
        this.subs = []       //该事件下被订阅对象的集合
    }
    defined(){              // 添加订阅者
        Dep.watch.add(this);
    }
    notify() {              //通知订阅者有变化
        this.subs.forEach((e, i) => {
            if(typeof e.update === 'function'){
                try {
                   e.update.apply(e)  //触发订阅者更新函数
                } catch(err){
                    console.warr(err)
                }
            }
        })
    }
    
}
Dep.watch = null;

class Watch {
    constructor(name, fn){
        this.name = name;       //订阅消息的名称
        this.id = new Date();   //这里简单的运用时间戳做订阅者的ID
        this.callBack = fn;     //订阅消息发送改变时->订阅者执行的回调函数     
    }
    add(dep) {                  //将订阅者放入dep订阅池
       dep.subs.push(this);
    }
    update() {                  //将订阅者更新方法
        var cb = this.callBack; //赋值为了不改变函数内调用的this
        cb(this.name);          
    }
}     

重新封装history方法,并添加addHistoryListener方法以支持自定义监听。


const addHistoryMethod= (function(){
 var historyDep = new Dep()// 创建订阅池
 return function (name){
  if(name==='historyChange'){
   var event = new Watch(name,fn);
   Dep.watch = evnet;
   historyDep.defind();//增加订阅者
   Dep.watch = null;
  }else if(name==='pushState'||name==='replaceState'){
   var method = history[name];
   return function(){
    method.apply(history,argumnets)
    historyDep.notify();
   }
  }
 }
})()
window.addHistoryListener = addHistoryMethod('historyChange')
history.pushState =  addHistoryMethod('pushState');
history.replaceState =  addHistoryMethod('replaceState');

封装完成后的测试用例如下:


window.addHistoryListener('history',function(){
 console.log('窗口history改变了')
})
window.addHistoryListerer('history',function(){
 console.log('窗口history改变,我也听到了')// 可绑定多个监听事件
 console.log(history.state)
})
history.pushState({foo:bar}, 'title', '/car')

获取当前状态

页面加载时,可能会存在一个非null的状态对象。这种情况可能发生,例如:页面通过pushState()或replaceState()设置了状态对象,随后用户重启了浏览器。此时,页面重新加载时会触发onload事件,但不会触发popstate事件。然而,如果读取history.state属性,你将获得与触发popstate事件时相同的状态对象。

你可以直接读取当前历史记录项的状态对象state,而无需等待popstate事件,只需使用history.state属性即可:


let currentState = history.state;

对比分析

pushState replaceState
会在当前页面创建并激活新的历史记录,点击返回按钮会返回到之前的url 会修改当前的历史记录,按回退按钮,会跳过被修改的url

总结

History API是实现无刷新路由的有效方式,特别适用于单页面应用开发,有助于保障系统体验的稳定性。然而,原生History API不监听pushState和replaceState的行为,仅监听go、back、forward等操作。为解决这一问题,我们可以采用订阅发布模式,利用Dep和Watch对pushState和replaceState进行重新封装。在调用这些方法后,会自动触发notify方法,并增加addHistoryListener方法用于添加监听回调(即订阅者)。

到此这篇关于详解JavaScript实现路由变化的文章就介绍到这了,更多相关JavaScript监听路由变化内容请搜索本站以前的文章或继续浏览下面的相关文章希望大家以后多多支持本站!

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章