0
点赞
收藏
分享

微信扫一扫

图解 history api 和 React Router 实现原理

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 实现原理_JavaScript_02

history.length 是 5

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

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

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

就会回到这里:

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

这时候 history.length 依然是 5

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

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

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

图解 history api 和 React Router 实现原理_javascript_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 实现原理_ide_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 实现原理_前端_14

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

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

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

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

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

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

也就是变成了这样:

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

为什么呢?

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

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

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

这时候 history.length 就是 3 了。

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

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

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

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

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

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

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

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

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

这是因为它的 history.scrollRestoration 是 auto

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

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

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

图解 history api 和 React Router 实现原理_React.js_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 实现原理_javascript_27

然后我监听 popstate 事件:

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

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

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

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

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

图解 history api 和 React Router 实现原理_JavaScript_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 实现原理_React.js_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 实现原理_javascript_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 实现原理_javascript_37

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

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

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

那它是怎么实现的呢?

我们断点调试下:

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

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

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

在 createBrowserRouter 的地方打个断点:

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

点击 debug:

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

代码会在这里断住:

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

点击 step into 进入函数内部:

它调用了 createRouter:

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

这里传入了 history。

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

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

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

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

listen 就是监听 popstate 事件。

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

图解 history api 和 React Router 实现原理_React.js_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 实现原理_ide_51

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

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

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

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

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

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

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

也就是这样的流程:

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

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

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

会执行 navigate 方法:

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

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

然后又到了 matchRoutes 的流程:

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

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

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

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

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

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

也就是这样的流程:

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

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

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

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

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

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

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

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

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

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

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

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

后续流程一样。

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

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

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

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

效果一样。

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

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

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

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

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

图解 history api 和 React Router 实现原理_前端_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 是密不可分的。

举报

相关推荐

0 条评论