React | React Router 路由切换原理

简介: React | React Router 路由切换原理

前言


此前一直在疑惑,明明 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


原因剖析


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

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

6.webp.jpg


本文讨论的是 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 对象的一些属性和方法:

7.webp.jpg


先回到 <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.

目录
相关文章
|
2月前
|
JSON 缓存 前端开发
【React】React原理面试题集锦
本文集合一些React的原理面试题,方便读者以后面试查漏补缺。作者给出自认为可以让面试官满意的简易答案,如果想要了解更深刻,可以点击链接查看对应的详细博文。在此对链接中的博文作者非常感谢🙏。
37 0
|
3月前
|
前端开发 JavaScript
React和Vue实现路由懒加载
React和Vue实现路由懒加载
36 2
|
18天前
|
存储 人工智能 开发框架
【AI大模型应用开发】【AutoGPT系列】0. AutoGPT概念及原理介绍 - Agent开发框架及ReAct方法
【AI大模型应用开发】【AutoGPT系列】0. AutoGPT概念及原理介绍 - Agent开发框架及ReAct方法
22 0
|
29天前
|
前端开发
【掰开揉碎】React Router——React应用导航(二)
【掰开揉碎】React Router——React应用导航(二)
|
29天前
|
资源调度 前端开发 网络架构
【掰开揉碎】React Router——React应用导航(一)
【掰开揉碎】React Router——React应用导航(一)
|
1月前
|
前端开发 JavaScript 算法
React原理
【4月更文挑战第4天】本文介绍了React的核心概念,包括jsx、React.createElement和fiber。jsx是React的语法糖,被转换为React.createElement生成虚拟DOM (vDOM)以优化性能。vDOM是轻量的数据结构,用于描述DOM状态。React通过fiber结构改进渲染性能,将同步任务拆分成小任务,利用requestIdleCallback在浏览器空闲时执行,确保流畅的用户体验。fiber是增强的vDOM,包含额外的引用指针。文章还提及了diff算法和hooks在React中的作用。
11 0
|
3月前
|
前端开发 中间件 数据安全/隐私保护
React路由进阶方法
React路由进阶方法
28 1
|
3月前
|
前端开发 JavaScript
前端框架React原理
前端框架React原理
33 0
|
3月前
|
前端开发 JavaScript API
介绍一下React Router的工作原理。
介绍一下React Router的工作原理。
33 5
|
3月前
|
JavaScript 前端开发 网络安全
vue与react路由拦截
vue与react路由拦截
30 4