xiaochengzi6 / Blog

个人博客
GNU Lesser General Public License v2.1
0 stars 0 forks source link

React Router 原理 #15

Open xiaochengzi6 opened 2 years ago

xiaochengzi6 commented 2 years ago

写作目的:路由器的学习总是知其然不知所以然所以想借机好好了解一下器内部的原理。我草草看了一些文章我对其总结为 history ——> 包裹(react) ——> react-router

它有两种模式 1.hash 2.history

这两种模式对应着BOM对象中的两个对象 1.location 2.history

1.1 location :它提供当前窗口加载文档的有关信息还提供了一些导航功能。

location能够将 URL 解析为独立的片段

// https://juejin.cn/post/6886290490640039943#heading-6
location.hash // #heading-6
location.href // https://juejin.cn/post/6886290490640039943#heading-6
location.pathname // /post/6886290490640039943
location.search // 返回 ?后的字符包括 ?

每次改变 location 的属性都会已新的 URL 来重新加载 除了 hash 之外。[官方Location][https://developer.mozilla.org/zh-CN/docs/Web/API/Location]

location.hash = '#iii'
location.search = '?iii'
location.replace()
location.reload()

通过这样的方式会在历史记录中保存下来。 使用 replace() 不会保存在历史记录中 使用 reload() 通过最有效的方式来加载页面。可能是在缓存中(参数:false) 也可能在服务器中 (参数:true)

主要使用了 location.hash 来改变路由

1.2 history : 它保存着用户的上网历史记录。它是window的属性因此只要有window的出现就会存在history。

[官方History][https://developer.mozilla.org/zh-CN/docs/Mozilla/Add-ons/WebExtensions/API/history]

history.go()
history.back()
history.forword()

history.pushState(state, title, url) // 向历史记录中加一个记录
history.repalceState(state, title, url) // 更新历史栈上最新的入口
// 1.state 一个与指定网址相关的状态对象, popstate 事件触发时,该对象会传入回调函数。如果不需要可填 null。
//2. title 新页面的标题,但是所有浏览器目前都忽略这个值,可填 null。
//3.url 新的网址,必须与当前页面处在同一个域。浏览器的地址栏将显示这个地址。但不会及时加载这个网址。

使用 history.pushState() 可以对比于 location.hash() 但前者优点有很多。

需要注意的是调用history.pushState()history.replaceState()不会触发popstate事件。只有在做出浏览器动作时,才会触发该事件,如用户点击浏览器的回退按钮

主要使用了 history.pushState()history.replaceState() 来改变路由

2.1 history

重点明白 history 在干什么 它主要是负责记录路由history状态,以及path改变了,在history模式下用popstate监听路由变化,在hash模式下用hashchange监听路由的变化。

img

BrowserRouter 组件的代码

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     // 将history的location挂载到state上
    };

    // 下面两个变量是防御性代码,防止根组件还没渲染location就变了
    // 如果location变化时,当前根组件还没渲染出来,就先记下他,等当前组件mount了再设置到state上
    this._isMounted = false;
    this._pendingLocation = null;

    // 通过history监听路由变化,变化的时候,改变state上的location
    this.unlisten = props.history.listen(location => {
      if (this._isMounted) {
        this.setState({ location });
      } else {
        this._pendingLocation = location;
      }
    });
  }

  componentDidMount() {
    this._isMounted = true;

    if (this._pendingLocation) {
      this.setState({ location: this._pendingLocation });
    }
  }

  componentWillUnmount() {
    if (this.unlisten) {
      this.unlisten();
      this._isMounted = false;
      this._pendingLocation = null;
    }
  }

  render() {
    // render的内容很简单,就是两个context
    // 一个是路由的相关属性,包括history和location等
    // 一个只包含history信息,同时将子组件通过children渲染出来
    return (
      <RouterContext.Provider
        value={{
          history: this.props.history,
          location: this.state.location,
          match: Router.computeRootMatch(this.state.location.pathname),
        }}
      >
        <HistoryContext.Provider
          children={this.props.children || null}
          value={this.props.history}
        />
      </RouterContext.Provider>
    );
  }
}

export default Router;

2.2分析一下流程:

2.2.1 当浏览器地址栏发生改变或者点击前进后退会发生什么变化。

一、history 模式下

  1. URL 发生变化(会触发 popstate 事件做出一些动作 )开始触发 history 下的 setState 函数产生新的 location 对象。
  2. 然后通知Router组件开始更新 location 并通过context 上下文传递。
  3. Switch 通过传递的 context 匹配出符合 Router 组件 最后有 Router 组件取出context 内容传递到渲染页面,渲染更新。

二、hash 模式下

类似....

2.2.2当使用 history.push 方法时,切换路由,又会发生了什么?

一、history 模式下

调用 history.pushState() 方法改变当前的 URL 。(会触发监听事件做出一些动作 )触发 history 下的 setState 函数产生新的 location 对象。然后通知Router组件开始更新 location 并通过context 上下文传递。 Switch 通过传递的 context 匹配出符合 Router 组件 最后有 Router 组件取出context 内容传递到渲染页面,渲染更新。

二、hash 模式下

类似....

总结一下:

React-Router实现时核心逻辑如下:

  1. 使用不刷新的路由API,比如history.replaceState() history.pushState()或者location.hash

  2. 提供一个事件处理机制,让React组件可以监听路由变化。

    1.当使用history.replaceState()或者history.pushState()时候不会触发 popstate 事件。这个时候采用了发布-订阅的模式来监听这些事件

    2.当点击浏览器的前景后退时候就会触发 popstate 事件 并开始监听

  3. 提供操作路由的接口,当路由变化时,通过事件回调通知React

  4. 当路由事件触发时,将变化的路由写入到React的响应式数据上,也就是将这个值写到根routerstate上,然后通过context传给子组件。

  5. 具体渲染时将路由配置的path和当前浏览器地址做一个对比,匹配上就渲染对应的组件。

img

参考资料

https://juejin.cn/post/6886290490640039943#heading-29

https://juejin.cn/post/6855129007949398029#heading-8

xiaochengzi6 commented 1 year ago

更详细的 react-router 的源码分析查看此仓库