细读 React | React Router 路由切换原理

2022 北京冬奥会开幕式

此前一直在疑惑,明明 pushState()replaceState() 不触发 popstate 事件,可为什么 React Router 还能挂载对应路由的组件呢?

翻了一下 history.js 源码,终于知道原因了。

源码

假设项目路由设计如下:

import { render } from 'react-dom'
import { BrowserRouter, Routes, Route } from 'react-router-dom'
import { Mine, About } from './routes'
import App from './App'

const rootElement = document.getElementById('root')

render(
  <BrowserRouter>
    <Routes>
      <Route path="/" exact element={<App />} />
      <Route path="/mine" element={<Mine />} />
      <Route path="/about" element={<About />} />
    </Routes>
  </BrowserRouter>,
  rootElement
)

然后我们看下 <BrowserRouter /> 的源码(react-router-dom/modules/BrowserRouter.js),以下省略了一部分无关代码:

import React from 'react'
import { Router } from 'react-router'
import { createBrowserHistory as createHistory } from 'history'

/**
 * The public API for a <Router> that uses HTML5 history.
 */
class BrowserRouter extends React.Component {
  // 构建 history 对象
  history = createHistory(this.props)

  render() {
    // 将 history 对象等传入 <Router /> 组件
    return <Router history={this.history} children={this.props.children} />
  }
}

// ...

export default BrowserRouter

接着我们继续看下 <Router /> 组件的源码(react-router/modules/Router.js),如下:

import React from 'react'
import HistoryContext from './HistoryContext.js'
import RouterContext from './RouterContext.js'

/**
 * The public API for putting history on context.
 */
class Router extends React.Component {
  static computeRootMatch(pathname) {
    return { path: '/', url: '/', params: {}, isExact: pathname === '/' }
  }

  constructor(props) {
    super(props)

    this.state = {
      location: props.history.location
    }
    
    // 关键点:
    // 当触发 popstate 事件、
    // 或主动调用 props.history.push()、props.history.replace() 方法时,
    // 都会执行 history 对象的 listen 方法,使得执行 setState 强制更新当前组件
    this.unlisten = props.history.listen(location => {
      this.setState({ location })
    })
  }

  componentWillUnmount() {
    // 组件卸载时,解除监听
    if (this.unlisten) this.unlisten()
  }

  render() {
    return (
      // 由于 React Context 的特性,所有消费 RouterContext.Provider 的 Custom 组件
      // 在其 value 值发生变化时,都会重新渲染。
      // 当前 <Router /> 组件并没有做任何限制重新渲染的处理,
      // 因此每次 setState 都会引起 RouterContext.Provider 的 value 值发生变化。
      <RouterContext.Provider
        value={{
          history: this.props.history,
          location: this.state.location,
          match: Router.computeRootMatch(this.state.location.pathname),
          staticContext: this.props.staticContext
        }}
      >
        <HistoryContext.Provider children={this.props.children || null} value={this.props.history} />
      </RouterContext.Provider>
    )
  }
}

export default Router

原因剖析

往下之前,如果对 History API 或者 URL Fragment 不了解的,可以看下这篇文章:History 对象及事件监听详解。

react-router-dom 引用了 history.js 库 ,它主要提供了三种方法:createBrowserHistorycreateHashHistorycreateMemoryHistory

它们用于构建对应模式的 history 对象(请注意,它有别于 window.history 对象),该对象的属性和方法可在 Devtools 中清晰地看到(如下图),也可查阅文档。这个太简单了,你们都懂,不说了。

本文讨论的是 History 模式,因而对应 createBrowserHistory 方法。

在构建项目路由时,选择 <BrowserRouter /> 组件,它内部是将通过 createBrowserHistory() 方法构造的 history 对象传递给 <Router /> 组件。

我们知道,在 React 应用中切换路由,它会加载对应的组件。我们知道 createBrowserHistory() 利用了 HTML5 History API 特性,但是主动调用 window.history.pushState()window.history.replaceState() 方法都不会触发 popstate 事件,因此,如果仅通过监听 popstate 事件是不能完全实现路由切换的。

那么 React Router 是如何解决问题的呢?

在前面的源码部分,其实已经添加了一些注解,<Router /> 组件它内部依赖于 Context 的 Provider/Comsumer 模式。因此,它只要做到 URL 发生变化时更新 Context.Providervalue 值即可,至于后续如何加载组件就交给 React 了(当然里面还包括 React Router 的路由匹配,但非本文讨论内容,不展开讲述)。

一般情况下,<BrowserRouter /> 都会作为整个项目的根路由,它包裹了一层 <Router /> 组件,<Router /> 组件在实例化时,设置了一个监听函数:

// props.history 就是通过 createBrowserHistory(props) 生成的对象
this.unlisten = props.history.listen(location => {
  // 回调函数的作用是,通过 setState 触发 Router 组件更新,
  // 使得 Provider 的 value 值发生变化,以带动 Consumer 的更新。
  this.setState({ location })
})
// this.unlisten 是一个函数,执行它内部会移除 popstate 事件监听器

Q:history.js 是如何做到每当 URL 发生变化,会触发这个回调函数的?

在 React 中是通过调用组件的 props.history.push()props.history.replace() 方法实现路由切换的。

我们来看一下 history.js 的源码(history/esm/history.js):

里面省略了一部分代码,然后分析顺序已经按顺序标注出来。

function createTransitionManager() {
  // ...

  function confirmTransitionTo(location, action, getUserConfirmation, callback) {
    var result = typeof prompt === 'function' ? prompt(location, action) : prompt

    if (typeof result === 'string') {
      if (typeof getUserConfirmation === 'function') {
        getUserConfirmation(result, callback)
      } else {
        process.env.NODE_ENV !== 'production' ? warning(false, 'A history needs a getUserConfirmation function in order to use a prompt message') : void 0
        callback(true)
      }
    } else {
      // Return false from a transition hook to cancel the transition.
      callback(result !== false)
    }
  }

  var listeners = []

  function appendListener(fn) {
    var isActive = true

    function listener() {
      if (isActive) fn.apply(void 0, arguments)
    }

    // 添加监听器
    listeners.push(listener)
    return function () {
      isActive = false
      // 过滤重复的监听器
      listeners = listeners.filter(function (item) {
        return item !== listener
      })
    }
  }

  // 6️⃣ 执行 listeners 中所有的 listener 监听器,
  // 最后触发 <Router /> 中的回调函数 this.unlisten = props.history.listen(location => { this.setState({ location }) }) 逻辑
  function notifyListeners() {
    // 将类数组 arguments 转换为数组形式
    for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
      args[_key] = arguments[_key]
    }

    listeners.forEach(function (listener) {
      // 回调函数将获得 location、action 两个参数
      return listener.apply(void 0, args)
    })
  }

  return {
    setPrompt: setPrompt,
    confirmTransitionTo: confirmTransitionTo,
    appendListener: appendListener,
    notifyListeners: notifyListeners
  }
}

/**
 * Creates a history object that uses the HTML5 history API including
 * pushState, replaceState, and the popstate event.
 */
function createBrowserHistory(props) {
  // ...

  // 创建 location 对象
  function getDOMLocation(historyState) {
    var _ref = historyState || {},
      key = _ref.key,
      state = _ref.state

    var _window$location = window.location,
      pathname = _window$location.pathname,
      search = _window$location.search,
      hash = _window$location.hash
    var path = pathname + search + hash

    if (basename) path = stripBasename(path, basename)
    return createLocation(path, state, key)
  }

  // ...

  // 创建 transitionManager 对象
  var transitionManager = createTransitionManager()

  // 5️⃣ 主要更新 history 对象,并调用 notifyListeners 方法
  function setState(nextState) {
    _extends(history, nextState)

    history.length = globalHistory.length
    // 执行 transitionManager 中的所有 listeners
    transitionManager.notifyListeners(history.location, history.action)
  }

  // 3️⃣ popstate 事件监听器的处理函数
  function handlePopState(event) {
    // getDOMLocation 方法用于生成 location 对象,location: { hash, pathname, search, state }
    // handlePop 方法,主要是用于触发 setState 方法
    handlePop(getDOMLocation(event.state))
  }

  // 4️⃣ 用于调用 setState 方法
  function handlePop(location) {
    var action = 'POP'
    transitionManager.confirmTransitionTo(location, action, getUserConfirmation, function (ok) {
      if (ok) {
        setState({
          action: action,
          location: location
        })
      }
    })
  }

  // 7️⃣
  // 这里的 push 和 replace 方法,是利用了 window.history.pushState() 和 window.history.replaceState()
  // 他们不会触发 popstate 事件,因此无法执行 handlePopState 方法,因此我们需要主动执行 setState() 方法,进而
  // 执行 notifyListeners() 以使得 <Router /> 组件的回调被执行,使得组件进行更新。
  function push(path, state) {
    // ...
    var action = 'PUSH'
    var location = createLocation(path, state, createKey(), history.location)
    // 将会执行 confirmTransitionTo 的 callback 函数
    transitionManager.confirmTransitionTo(location, action, getUserConfirmation, function (ok) {
      if (!ok) return
      var href = createHref(location)
      var key = location.key,
        state = location.state

      if (canUseHistory) {
        globalHistory.pushState(
          {
            key: key,
            state: state
          },
          null,
          href
        )

        if (forceRefresh) {
          window.location.href = href
        } else {
          var prevIndex = allKeys.indexOf(history.location.key)
          var nextKeys = allKeys.slice(0, prevIndex + 1)
          nextKeys.push(location.key)
          allKeys = nextKeys
          // 调用 setState() 方法,然后里面会执行 notifyListeners 方法,并触发 listeners 的所有监听器
          setState({
            action: action,
            location: location
          })
        }
      } else {
        window.location.href = href
      }
    })
  }

  function replace(path, state) {
    // 与 push 方法同理,省略...
  }

  // 8️⃣
  // 这里的 go()、goBack()、goForward() 全是利用了 History API 的能力,
  // 他们都会触发 popstate 事件,因此都会执行 handlePopState 方法。
  function go(n) {
    globalHistory.go(n)
  }

  function goBack() {
    go(-1)
  }

  function goForward() {
    go(1)
  }

  var listenerCount = 0

  // 2️⃣ 注册/移除 popstate 事件监听器
  function checkDOMListeners(delta) {
    listenerCount += delta

    if (listenerCount === 1 && delta === 1) {
      // 添加 popstate 事件监听器,执行 handlePopState 时将会触发 setState
      window.addEventListener(PopStateEvent, handlePopState)
      if (needsHashChangeListener) window.addEventListener(HashChangeEvent, handleHashChange)
    } else if (listenerCount === 0) {
      // 移除事件监听器
      window.removeEventListener(PopStateEvent, handlePopState)
      if (needsHashChangeListener) window.removeEventListener(HashChangeEvent, handleHashChange)
    }
  }

  // 1️⃣ 设置监听器,以触发 <Router /> 组件中的回调函数
  function listen(listener) {
    // 往 transitionManager 中的 listeners 数组添加新的监听器 listener,
    // 其中 transitionManager 对象有这些方法:{ setPrompt, confirmTransitionTo, appendListener, notifyListeners }
    var unlisten = transitionManager.appendListener(listener)

    // 负责添加、移除 popstate 事件监听器
    checkDOMListeners(1)

    // 执行回调函数移除 listener 监听器
    return function () {
      checkDOMListeners(-1)
      unlisten()
    }
  }

  var history = {
    length: globalHistory.length,
    action: 'POP',
    location: initialLocation,
    createHref: createHref,
    push: push,
    replace: replace,
    go: go,
    goBack: goBack,
    goForward: goForward,
    block: block,
    listen: listen
  }
  return history
}

以下是 history.js 中创建的 historylocation 对象的一些属性和方法:

先回到 <Router /> 组件中的 history.listen(fn),它主要做几件事:

  • fn 保存在负责存储监听器的 listeners 数组中,未来它将会被 notifyListeners() 方法调用。
  • 注册 popstate 事件监听器,触发之后,会执行 notifyListeners() 方法
  • 在 React 组件中调用 props.history.push() 等方法,也将会触发 notifyListeners() 方法。
  • 执行 notifyListeners() 方法,会执行 listeners 中所有的 listener,因此 fn 将会被触发。
  • 执行 fn() 触发 Component 中的 setState() 方法更新 <Router /> 组件,即 Router.Providervalue 发生改变,那么 Router.Consumer 就会跟着更新

所以,React Router 是利用了 Context 的 Provider/Custom 特性,解决了 pushState/replaceState 不触发 popstate 事件时实现了路由切换的问题。

The end.



喜欢的朋友记得点赞、收藏、关注哦!!!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/968118.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Flutter 双屏双引擎通信插件加入 GitCode:解锁双屏开发新潜能

在双屏设备应用场景日益丰富的当下&#xff0c;移动应用开发领域迎来了新的机遇与挑战。如何高效利用双屏设备优势&#xff0c;为用户打造更优质的交互体验&#xff0c;成为开发者们关注的焦点。近日&#xff0c;一款名为 Flutter 双屏双引擎通信插件的创新项目正式入驻 GitCod…

【C++高并发服务器WebServer】-18:事件处理模式与线程池

本文目录 一、事件处理模式1.1 Reactor模式1.2 Proactor模式1.3 同步IO模拟Proactor模式 二、线程池 一、事件处理模式 服务器程序通常需要处理三类事件&#xff1a;I/O事件、信号、定时事件。 对应的有两种高效的事件处理模式&#xff1a;Reactor和Proactor&#xff0c;同步…

人岗匹配为核,打造精确高效招聘 “高速路”

人才的选拔与招聘是企业开展所有工作的前提&#xff0c;通过选聘合适的人才&#xff0c;充分发挥其能力和潜质&#xff0c;帮助企业不断完成发展目标。尤其对于初创企业&#xff0c;在人力资源与财务状况均相对紧张的背景下&#xff0c;聚焦于关键岗位的人才招聘显得尤为重要。…

网络在线考试|基于vue的网络在线考试系统的设计与实现(源码+数据库+文档)

网络在线考试系统 目录 基于SSM&#xff0b;vue的网络在线考试系统的设计与实现 一、前言 二、系统设计 三、系统功能设计 1功能页面实现 2系统功能模块 3管理员功能模块 4学生功能模块 四、数据库设计 五、核心代码 六、论文参考 七、最新计算机毕设选题推荐 八…

vue2 导出Excel文件

1.安装依赖 npm install xlsx file-saver 2.使用 <template><button click"exportToExcel">导出Excel</button> </template><script> import * as XLSX from xlsx; import { saveAs } from file-saver; export default {methods: {ex…

第三届通信网络与机器学习国际学术会议(CNML 2025)

在线投稿&#xff1a; 学术会议-学术交流征稿-学术会议在线-艾思科蓝 通信网络机器学习 通信理论 通信工程 计算机网络和数据通信 信息分析和基础设施 通信建模理论与实践 无线传感器和通信网络 云计算与物联网 网络和数据安全 光电子学和光通信 无线/移动通信和技术 智能通信…

【漫话机器学习系列】085.自助采样法(Bootstrap Sampling)

自助采样法&#xff08;Bootstrap Sampling&#xff09; 1. 引言 在统计学和机器学习领域&#xff0c;数据的充足性直接影响模型的性能。然而&#xff0c;在许多实际场景中&#xff0c;我们可能无法获得足够的数据。为了解决这个问题&#xff0c;自助采样法&#xff08;Boots…

Ai无限免费生成高质量ppt教程(deepseek+kimi)

第一步&#xff1a;打开deepseek官网&#xff08;DeepSeek) 1.如果deepseek官网网络繁忙&#xff0c;解决方案如下&#xff1a; (1)超算互联网:DeepSeek (scnet.cn) (2)秘塔AI搜索:https://metaso.cn/(开启长思考&#xff09; (3)纳米ai:https://bot.n.cn/ (4)使用easychat官网…

spring cloud 使用 webSocket

1.引入依赖,(在微服务模块中) <!-- Spring WebSocket --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-websocket</artifactId></dependency> 2.新建文件 package com.ruoyi.founda…

运行npm install卡住不动的

首先检查npm代理&#xff0c;是否已经使用国内镜像 // 执行以下命令查看是否为国内镜像 npm config get registry 如果不是则换成国内镜像&#xff0c;执行以下命令 npm config set registryhttps://registry.npmmirror.com //执行以下命令查看是否配置成功 npm config get …

DeepSeek Coder + IDEA 辅助开发工具

开发者工具 我之前用的是Codegeex4模型&#xff0c;现在写一款DeepSeek Coder 本地模型 DeepSeek为什么火&#xff0c;我在网上看到一个段子下棋DeepSeek用兵法赢了ChatGpt&#xff0c;而没有用技术赢&#xff0c;这就是AI的思维推理&#xff0c;深入理解孙子兵法&#xff0c…

基于 PyTorch 的树叶分类任务:从数据准备到模型训练与测试

基于 PyTorch 的树叶分类任务&#xff1a;从数据准备到模型训练与测试 1. 引言 在计算机视觉领域&#xff0c;图像分类是一个经典的任务。本文将详细介绍如何使用 PyTorch 实现一个树叶分类任务。我们将从数据准备开始&#xff0c;逐步构建模型、训练模型&#xff0c;并在测试…

11vue3实战-----封装缓存工具

11vue3实战-----封装缓存工具 1.背景2.pinia的持久化思路3.以localStorage为例解决问题4.封装缓存工具 1.背景 在上一章节&#xff0c;实现登录功能时候&#xff0c;当账号密码正确&#xff0c;身份验证成功之后&#xff0c;把用户信息保存起来&#xff0c;是用的pinia。然而p…

vue中使用高德地图自定义掩膜背景结合threejs

技术架构 vue3高德地图2.0threejs 代码步骤 这里我们就用合肥市为主要的地区&#xff0c;将其他地区扣除&#xff0c;首先使用高德的webapi的DistrictSearch功能&#xff0c;使用该功能之前记得检查一下初始化的时候是否添加到plugins中&#xff0c;然后搜索合肥市的行政数据…

02、QLExpress从入门到放弃,相关API和文档

QLExpress从入门到放弃,相关API和文档 一、属性开关 public class ExpressRunner {private boolean isTrace;private boolean isShortCircuit;private boolean isPrecise; }/*** 是否需要高精度计算*/ private boolean isPrecise false;高精度计算在会计财务中非常重要&…

二、OSG学习笔记-入门开发

前一章节&#xff1a;一、OSG学习笔记-编译开发环境-CSDN博客https://blog.csdn.net/weixin_36323170/article/details/145513691 一、环境配置 1、VS需要配置头文件路径如下图&#xff1a;&#xff08;$(OSG_INCLUDE)&#xff09; 这里的OSG_INCLUDE,为环境变量名&#xff0…

C++ Primer 语句作用域

欢迎阅读我的 【CPrimer】专栏 专栏简介&#xff1a;本专栏主要面向C初学者&#xff0c;解释C的一些基本概念和基础语言特性&#xff0c;涉及C标准库的用法&#xff0c;面向对象特性&#xff0c;泛型特性高级用法。通过使用标准库中定义的抽象设施&#xff0c;使你更加适应高级…

Windows逆向工程入门之汇编开发框架解析

公开视频 -> 链接点击跳转公开课程博客首页 -> ​​​链接点击跳转博客主页 目录 环境搭建与配置 Visual Studio配置 X86汇编基础框架 基本程序框架 数据定义与内存访问 过程&#xff08;函数&#xff09;定义 汇编框架解析 代码主体解析 完整代码执行 代码逻…

Android ndk兼容 64bit so报错

1、报错logcat如下 2025-01-13 11:34:41.963 4687-4687 DEBUG pid-4687 A #01 pc 00000000000063b8 /system/lib64/liblog.so (__android_log_default_aborter16) (BuildId: 467c2038cdfa767245f9280e657fdb85) 2025…

工业路由器物联网应用,智慧环保环境数据监测

在智慧环保环境数据监测中工业路由器能连接各类分散的传感器&#xff0c;实现多源环境数据集中采集&#xff0c;并通过多种通信网络稳定传输至数据中心或云平台。 工作人员借助工业路由器可远程监控设备状态与环境数据&#xff0c;还能远程配置传感器参数。远程控制设置数据阈…