首页 > 其他分享 >图解 history api 和 React Router 实现原理

图解 history api 和 React Router 实现原理

时间:2023-08-14 18:33:31浏览次数:40  
标签:popstate react 渲染 React api go Router pushState history

Router 是开发 React 应用的必备功能,那 React Router 是怎么实现的呢?

今天我们就来读一下 React Router 的源码吧!

首先,我们来学一下 History API,这是基础。

什么是 history 呢?

就是这个东西:

图解 history api 和 React Router 实现原理_ide

我打开了一个新的标签页、然后访问 baidu.com、sougou.com、taobao.com。

长按后退按钮,就会列出历史记录,这就是 history。

现在在这里:

图解 history api 和 React Router 实现原理_前端_02

history.length 是 5

图解 history api 和 React Router 实现原理_前端_03

点击两次后退按钮,或者执行两次 history.back()

图解 history api 和 React Router 实现原理_前端_04

就会回到这里:

图解 history api 和 React Router 实现原理_JavaScript_05

这时候 history.length 依然是 5

图解 history api 和 React Router 实现原理_ide_06

因为前后的 history 都还保留着:

图解 history api 和 React Router 实现原理_React.js_07

图解 history api 和 React Router 实现原理_前端_08

除了用 history.back、history.forward 在 history 之间切换外,还可以用 history.go

参数值是 delta:

history.go(0) 是刷新当前页面。

history.go(1) 是前进一个,相当于 history.forward()

history.go(-1) 是后退一个,相当于 history.back()

当然,你还可以 history.go(-2)、histroy.go(3) 这种。

图解 history api 和 React Router 实现原理_前端_09

比如当我执行 history.go(-2) 的时候,能直接从 taobao.com 跳到 sogou.com

图解 history api 和 React Router 实现原理_前端_10

你还可以通过 history.replaceState 来替换当前 history:

图解 history api 和 React Router 实现原理_javascript_11

history.replaceState({aaa:1}, '', 'https://www.baidu.com?wd=光')

第一个参数是 state、第二个参数是 title,第三个是替换的 url。

不过第二个参数基本都不支持,state 倒是能拿到。

比如我在 www.baidu.com 那页 replaceState 为一个新的 url:

图解 history api 和 React Router 实现原理_JavaScript_12

前后 history 都没变,只有当前的变了:

图解 history api 和 React Router 实现原理_JavaScript_13

也就是这样:

图解 history api 和 React Router 实现原理_React.js_14

当然,你还可以用 history.pushState 来添加一个新的 history:

history.pushState({bbb:1}, '', 'https://www.baidu.com?wd=东');

图解 history api 和 React Router 实现原理_javascript_15

但有个现象,就是之后的 history 都没了:

图解 history api 和 React Router 实现原理_JavaScript_16

图解 history api 和 React Router 实现原理_javascript_17

也就是变成了这样:

图解 history api 和 React Router 实现原理_javascript_18

为什么呢?

因为你是 history.pushState 的时候,和后面的 history 冲突了,也就是分叉了:

图解 history api 和 React Router 实现原理_ide_19

这时候自然只能保留一个分支,也就是最新的那个。

这时候 history.length 就是 3 了。

图解 history api 和 React Router 实现原理_JavaScript_20

至此,history 的 length、go、back、forward、pushState、replaceState、state 这些 api 我们就用了一遍了。

还有个 history.scrollRestoration 是用来保留滚动位置的:

有两个值 auto、manual,默认是 auto,也就是会自动定位到上次滚动位置,设置为 manual 就不会了。

比如我访问百度到了这个位置:

图解 history api 和 React Router 实现原理_React.js_21

打开个新页面,再退回来:

图解 history api 和 React Router 实现原理_javascript_22

依然是在上次滚动到的位置。

这是因为它的 history.scrollRestoration 是 auto

图解 history api 和 React Router 实现原理_前端_23

我们把它设置为 manual 试试看:

图解 history api 和 React Router 实现原理_JavaScript_24

图解 history api 和 React Router 实现原理_前端_25

这时候就算滚动到了底部,再切回来也会回到最上面。

此外,与 history 相关的还有个事件:popstate

当你在 history 中导航时,popstate 就会触发,比如 history.forwad、histroy.back、history.go。

但是 history.pushState、history.replaceState 这种并不会触发 popstate。

我们测试下:

图解 history api 和 React Router 实现原理_javascript_26

history.pushState({aaa:1}, '', 'https://www.baidu.com?#/aaa');

history.pushState({bbb:2}, '', 'https://www.baidu.com?#/bbb');

我在 www.baidu.com 这个页面 pushState 添加了两个 history。

加上导航页一共 4 个:

图解 history api 和 React Router 实现原理_前端_27

然后我监听 popstate 事件:

图解 history api 和 React Router 实现原理_javascript_28

window.addEventListener('popstate', event => {console.log(event)});

执行 history.back 和 history.forward 都会触发 popstate 事件:

图解 history api 和 React Router 实现原理_ide_29

事件包含 state,也可以从 target.location 拿到当前 url

图解 history api 和 React Router 实现原理_前端_30

但是当你 history.pushState、history.replaceState 并不会触发它:

图解 history api 和 React Router 实现原理_React.js_31

也就是说添加、修改 history 不会触发 popstate,只有在 state 之间导航才会触发。

综上,history api 和 popstate 事件我们都过了一遍。

基于这些就可以实现 React Router。

有的同学说,不是还有个 hashchange 事件么?

确实,那个就是监听 hash 变化的。

图解 history api 和 React Router 实现原理_JavaScript_32

图解 history api 和 React Router 实现原理_ide_33

基于它也可以实现 router,但很明显,hashchange 只能监听 hash 的变化,而 popstate 不只是 hash 变化,功能更多。

所以用 popstate 事件就足够了。

其实在 react router 里,就只用到了 popstate 事件,没用到 hashchange 事件:

图解 history api 和 React Router 实现原理_javascript_34

图解 history api 和 React Router 实现原理_JavaScript_35

接下来我们就具体来看下 React Router 是怎么实现的吧。

创建个 react 项目:

npx create-react-app react-router-test

图解 history api 和 React Router 实现原理_前端_36

安装 react-router 的包:

npm install react-router-dom

然后在 index.js 写如下代码:

import React from 'react';
import ReactDOM from 'react-dom/client';
import {
  createBrowserRouter,
  Link,
  Outlet,
  RouterProvider,
} from "react-router-dom";

function Aaa() {
  return <div>
    <p>aaa</p>
    <Link to={'/bbb/111'}>to bbb</Link>
    <br/>
    <Link to={'/ccc'}>to ccc</Link>
    <br/>
    <Outlet/>
  </div>;
}

function Bbb() {
  return 'bbb';
}

function Ccc() {
  return 'ccc';
}

function ErrorPage() {
  return 'error';
}

const routes = [
  {
    path: "/",
    element: <Aaa/>,
    errorElement: <ErrorPage/>,
    children: [
      {
        path: "bbb/:id",
        element: <Bbb />,
      },
      {
        path: "ccc",
        element: <Ccc />,
      }    
    ],
  }
];
const router = createBrowserRouter(routes);

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<RouterProvider router={router} />);

通过 react-router-dom 包的 createBrowserRouter 创建 router,传入 routes 配置。

然后把 router 传入 RouterProvider。

有一个根路由 /、两个子路由 /bbb/:id 和 /ccc

把开发服务跑起来:

npm run start

测试下:

图解 history api 和 React Router 实现原理_React.js_37

子路由对应的组件在 <Outlet/> 处渲染。

当没有对应路由的时候,会返回错误页面:

图解 history api 和 React Router 实现原理_前端_38

那它是怎么实现的呢?

我们断点调试下:

图解 history api 和 React Router 实现原理_JavaScript_39

创建调试配置文件 launch.json,然后创建 chrome 类型的调试配置:

图解 history api 和 React Router 实现原理_JavaScript_40

在 createBrowserRouter 的地方打个断点:

图解 history api 和 React Router 实现原理_ide_41

点击 debug:

图解 history api 和 React Router 实现原理_前端_42

代码会在这里断住:

图解 history api 和 React Router 实现原理_ide_43

点击 step into 进入函数内部:

它调用了 createRouter:

图解 history api 和 React Router 实现原理_React.js_44

这里传入了 history。

这个不是原生的 history api,而是包装了一层之后的:

关注 listen、push、replace、go 这 4 个方法就好了:

图解 history api 和 React Router 实现原理_前端_45

图解 history api 和 React Router 实现原理_前端_46

listen 就是监听 popstate 事件。

而 push、replace、go 都是对 history 的 api 的封装:

图解 history api 和 React Router 实现原理_javascript_47

图解 history api 和 React Router 实现原理_JavaScript_48

此外,history 还封装了 location 属性,不用自己从 window 取了。

然后 createRouter 里会对 routes 配置和当前的 location 做一次 match:

图解 history api 和 React Router 实现原理_JavaScript_49

图解 history api 和 React Router 实现原理_JavaScript_50

matchRoutes 会把嵌套路由拍平,然后和 location 匹配:

图解 history api 和 React Router 实现原理_前端_51

然后就匹配到了要渲染的组件以及它包含的子路由:

图解 history api 和 React Router 实现原理_React.js_52

这样当组件树渲染的时候,就知道渲染什么组件了:

图解 history api 和 React Router 实现原理_javascript_53

就是把 match 的这个结果渲染出来。

这样就完成了路由对应的组件渲染:

图解 history api 和 React Router 实现原理_前端_54

也就是这样的流程:

图解 history api 和 React Router 实现原理_ide_55

当点击 link 切换路由的时候:

图解 history api 和 React Router 实现原理_React.js_56

会执行 navigate 方法:

图解 history api 和 React Router 实现原理_React.js_57

图解 history api 和 React Router 实现原理_前端_58

然后又到了 matchRoutes 的流程:

图解 history api 和 React Router 实现原理_JavaScript_59

match 完会 pushState 或者 replaceState 修改 history,然后更新 state:

图解 history api 和 React Router 实现原理_前端_60

然后触发了 setState,组件树会重新渲染:

图解 history api 和 React Router 实现原理_javascript_61

图解 history api 和 React Router 实现原理_React.js_62

也就是这样的流程:

图解 history api 和 React Router 实现原理_React.js_63

router.navigate 会传入新的 location,然后和 routes 做 match,找到匹配的路由。

之后会 pushState 修改 history,并且触发 react 的 setState 来重新渲染,重新渲染的时候通过 renderMatches 把当前 match 的组件渲染出来。

而渲染到 Outlet 的时候,会从 context 中取出当前需要渲染的组件来渲染:

图解 history api 和 React Router 实现原理_前端_64

图解 history api 和 React Router 实现原理_React.js_65

这就是 router 初次渲染和点击 link 时的渲染流程。

那点击前进后退按钮的时候呢?

这个就是监听 popstate,然后也做一次 navigate 就好了:

图解 history api 和 React Router 实现原理_前端_66

图解 history api 和 React Router 实现原理_ide_67

图解 history api 和 React Router 实现原理_前端_68

后续流程一样。

图解 history api 和 React Router 实现原理_React.js_69

回过头来,其实 react router 的 routes 其实支持这两种配置方式:

图解 history api 和 React Router 实现原理_JavaScript_70

图解 history api 和 React Router 实现原理_ide_71

效果一样。

看下源码就知道为什么了:

首先,这个 Route 组件就是个空组件,啥也没:

图解 history api 和 React Router 实现原理_javascript_72

而 Routes 组件里会从把所有子组件的参数取出来,变成一个个 route 配置:

图解 history api 和 React Router 实现原理_前端_73

图解 history api 和 React Router 实现原理_javascript_74

结果不就是和对象的配置方式一样么?

总结

我们学习了 history api 和 React Router 的实现原理。

history api 有这些:

  • length:history 的条数
  • forward:前进一个
  • back:后退一个
  • go:前进或者后退 n 个
  • pushState:添加一个 history
  • replaceState:替换当前 history
  • scrollRestoration:保存 scroll 位置,取值为 auto 或者 manual,manual 的话就要自己设置 scroll 位置了

而且还有 popstate 事件可以监听到 history.go、history.back、history.forward 的导航,拿到最新的 location。

这里要注意 pushState、replaceState 并不能触发 popstate 事件。也就是 history 之间导航(go、back、forward)可以触发 popstate,而修改 history (push、replace)不能触发。

React Router 就是基于这些 history api 实现的。

首次渲染的时候,会根据 location 和配置的 routes 做匹配,渲染匹配的组件。

之后点击 link 链接也会进行 location 和 routes 的匹配,然后 history.pushState 修改 history,之后通过 react 的 setState 触发重新渲染。

前进后退的时候,也就是执行 history.go、history.back、history.forward 的时候,会触发 popstate,这时候也是同样的处理,location 和 routes 的匹配,然后 history.pushState 修改 history,之后通过 react 的 setState 触发重新渲染。

渲染时会用到 Outlet组件 渲染子路由,用到 useXxx 来取一些匹配信息,这些都是通过 context 来传递的。

这就是 React Router 的实现原理,它和 history api 是密不可分的。

标签:popstate,react,渲染,React,api,go,Router,pushState,history
From: https://blog.51cto.com/u_15506823/7079939

相关文章

  • 一文详解Apipost数据模型功能
    在Apipost数据模型中用户可以预先创建多个数据模型,并在API设计过程中重复利用这些模型来构建API创建数据模型在左侧导航点击「数据模型」-「新建数据模型」在右侧工作台配置数据模型参数 引入数据模型在API设计预定义响应期望下点击引用数据模型,并选择需要导入的数据模型......
  • vue-router动态路由无限循环
    //isLogined用来判断用户是否已登录router.beforeEach((to,from,next)=>{if(isLogined){next()}else{console.log('测试')next('login')}})next()表示放行,直接进入to路由,不会再次调用router.beforeEach()next(path:...to,replace:true)拦截......
  • 如何在本地给 vue-router4 扩展功能?
    背景前段时间给基于vue3的H5项目做一些优化,该项目经常会有一些页面间的通信,譬如选择地址、乘机人等信息后回填到上一个页面。对于这种简单、频繁的通信,实在不想搞成重火力(eg:pinia)。最好让使用者用完即走,不用操心除业务逻辑之外的任何事情。路由控制页面通信既然是页面间的......
  • API接口对接电商平台高效获取各大电商平台数据,商品详情数据代码示例
     电商可以通过使用API接口获取商品信息数据。API是应用程序编程接口的缩写,它允许程序之间进行通信和数据传输。为了获取商品信息数据,电商可以利用API接口向商品供应商的数据库发送请求,然后接收并解析返回的数据。具体来说,电商可以按照以下步骤利用API接口获取商品信息数据:1.找......
  • NET web api 利用NPOI 读取excel
    安装NPOI`[HttpPost("users/upload")]publicasyncTaskUpload(IFormFilefile){if(file==null||file.Length==0)returnthis.BadRequest("文件未来上传");varapi_result=newList<string>();//文件......
  • 细谈商品详情API接口设计
    一、引言随着互联网技术的发展,商品详情信息的展示和交互变得越来越重要。为了提供更好的用户体验,我们需要设计一套高效、稳定且易于扩展的商品详情API接口。本文将详细探讨商品详情API接口的设计,包括接口的通用性、安全性和扩展性等方面,并附有相应的代码实现。二、商品详情API接......
  • 前端周刊第66期:TypeScript教程、ESM、React泡沫、htmx、测试文章
    周刊同步发表于微信公众号“写代码的宝哥”,欢迎各位小伙伴前来关注......
  • 修改并导出谷歌地图API中地图的样式
      本文介绍在谷歌地图API(GoogleMapsAPIs)中,设计地图样式并将设计好的样式通过JSON或URL导出的方法。  首先,进入GoogleMapsAPIs网站。在弹出的窗口中我们可以看到,目前还可以基于谷歌云端硬盘进行地图样式设计;但原有的GoogleMapsAPIs其实相对来说也还是很方便、简洁的,本文......
  • 怎么解释web api 是无状态
    WebAPI被称为无状态,这主要是指它不会保存客户端的任何数据。每一个请求需要携带所有必要的信息,因为WebAPI不会记住前一次请求的信息。它的设计是完全无状态的,每一次请求都是一个全新的交互,不依赖于任何历史上的信息或状态。对应的,这种无状态的规范让WebAPI更易扩展和管理,同时也......
  • ❤ React08-React 组件的生命周期3
    ❤React08-React组件的生命周期33组件卸载时(卸载阶段)执行时间:组件从页面之中消失时componentDidMount(){}组件的挂载阶段componentWillUnmount(){}组件的卸载阶段组件的挂载阶段执行的一些方法可以在组件的卸载阶段除去组件的其他钩子函数旧版本生命周期钩子函数新版......