前端路由解析(三) —— React-Router源码解析

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
简介: 前端路由解析(三) —— React-Router源码解析

640.jpg



写在前面

现在的前端应用很多都是单页应用。路由对于单页应用来说,是一个重要的组成部分。本系列文章将讲解前端路由的实现原理。这是系列文章的第三篇:React-Router源码解析。


前端路由解析(一) ——  hash路由

前端路由解析(二) ——  history路由


本文不会再介绍路由的基本原理,而是会结合React-Router的源码,探索一下路由和React是如何结合的。


示例代码


本文所用的React-Router的版本为:5.1.2,react版本为:16.12.0

我们用官方最简单的代码示例来分析一下React-Router的源码。


import {    BrowserRouter as Router,    Switch,    Route,    Link} from "react-router-dom";
export default function App() {    return (        <Router>            <div>                <nav>                    <ul>                        <li>                            <Link to="/">Home</Link>                        </li>                        <li>                            <Link to="/about">About</Link>                        </li>                        <li>                            <Link to="/users">Users</Link>                        </li>                    </ul>                </nav>
                {/* A <Switch> looks through its children <Route>s and            renders the first one that matches the current URL. */}                <Switch>                    <Route path="/about">                        <About />                    </Route>                    <Route path="/users">                        <Users />                    </Route>                    <Route path="/">                        <Home />                    </Route>                </Switch>            </div>        </Router>    );}
function Home() {    return <h2>Home</h2>;}
function About() {    return <h2>About</h2>;}
function Users() {    return <h2>Users</h2>;}


初次渲染


下面依次对 BrowserRouter、Switch、Route、Link进行解析。

BrowserRouter


import { Router } from 'react-router';
..._this.history = createBrowserHistory(_this.props);
...
_proto.render = function render() {  return React.createElement(Router, {    history: this.history,    children: this.props.children  });};


其中,Router来自react-router这个库,是react路由的核心组件,其内部维护了一个state。当页面的路由发生变化时,会更新state的location值,从而触发react的re-render。


/*interface Location<S = LocationState> {    pathname: Pathname;    search: Search;    state: S;    hash: Hash;    key?: LocationKey;} */
_this.state = {     location: props.history.location }


props.history,是createBrowserHistory这个函数生成的,createBrowserHistory是来自history这个package,返回了一个对象:


// 请记住这个history对象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;


Router组件渲染时,会将上述的方法、对象,传递给需要的childern组件。传递是通过 context 完成的。Router会在childern外包一层 Router.Provider,来提供history对象等信息。


// 这里的 context.Provider 是一个组件。使用的是 mini-create-react-context 这个 package。参考React.createContext
_proto.render = function render() {    return React.createElement(context.Provider, {      children: this.props.children || null,      value: {        history: this.props.history,        location: this.state.location,        match: Router.computeRootMatch(this.state.location.pathname),        staticContext: this.props.staticContext      }    });};


Switch


...
_proto.render = function render() {    var _this = this;
    return React.createElement(context.Consumer, null, function (context) {      !context ? process.env.NODE_ENV !== "production" ? invariant(false, "You should not use <Switch> outside a <Router>") : invariant(false) : void 0;      var location = _this.props.location || context.location;      var element, match; // We use React.Children.forEach instead of React.Children.toArray().find()       React.Children.forEach(_this.props.children, function (child) {        if (match == null && React.isValidElement(child)) {          element = child;          var path = child.props.path || child.props.from;          match = path ? matchPath(location.pathname, _extends({}, child.props, {            path: path          })) : context.match;        }      });      return match ? React.cloneElement(element, {        location: location,        computedMatch: match      }) : null;    });  };


Switch 会找到第一个匹配当前路由的Route,来进行渲染。Switch会在childern外面包一层Router.Comsumer。这个是为了通过context,拿到外层Router组件传递的history对象相关信息传递给Route组件。


Route


Route组件的逻辑也很好理解,首先是通过Router.Consumer拿到history对象等相关信息,经过一些处理后,在children外面包一层Router.Provider, 然后渲染children。之所以要包一层,我理解是为了供children中的Link组件等消费。来看代码:


...
proto.render = function render() {    var _this = this;
    return React.createElement(context.Consumer, null, function (context$1) {      !context$1 ? process.env.NODE_ENV !== "production" ? invariant(false, "You should not use <Route> outside a <Router>") : invariant(false) : void 0;      var location = _this.props.location || context$1.location;      var match = _this.props.computedMatch ? _this.props.computedMatch // <Switch> already computed the match for us      : _this.props.path ? matchPath(location.pathname, _this.props) : context$1.match;
      var props = _extends({}, context$1, {        location: location,        match: match      });
      var _this$props = _this.props,          children = _this$props.children,          component = _this$props.component,          render = _this$props.render; // Preact uses an empty array as children by      // default, so use null if that's the case.
      if (Array.isArray(children) && children.length === 0) {        children = null;      }
      return React.createElement(context.Provider, {        value: props      }, props.match ? children ? typeof children === "function" ? process.env.NODE_ENV !== "production" ? evalChildrenDev(children, props, _this.props.path) : children(props) : children : component ? React.createElement(component, props) : render ? render(props) : null : typeof children === "function" ? process.env.NODE_ENV !== "production" ? evalChildrenDev(children, props, _this.props.path) : children(props) : null);    });    ...


Link

Link组件最终会render一个包裹着Router.Consumer的LinkAnchor组件。Router.Consumer是为了获取外层Router组件的history对象等信息,LinkAnchor绑定了特殊的点击跳转逻辑的<a/>标签。这里先不展开了。


小结


初次渲染后,示例代码会形成下面这样的组件树:

640.png


分析了这么久,可以发现 :react-router-dom 主要的逻辑是处理history对象在整个react应用中的传递以及children的渲染等。history对象的传递是通过context完成的。history对象是由history 这个package中的createBrowserHistory函数生成的。而真正处理路由的核心逻辑, 是在 history 这个package中。

下面,我们来看看点击Link后,会发生什么?


点击Link



点击link后,最终会调用到history对象中的方法来进行路由的切换。


function navigate() {   var location = resolveToLocation(to, context.location);   // 这里的history,就是 createBrowserHistory 方法生成,并且在react组件树中传递的对象   var method = replace ? history.replace : history.push;   method(location);}

下面,进入history中的逻辑:


var href = createHref(location);var key = location.key,    state = location.state;
if (canUseHistory) {  // 调用window.history.pushState  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可不是react中的setState    setState({      action: action,      location: location    });  }}

我们来看下history中的setState干了什么:


function setState(nextState) {  // 更新history对象  _extends(history, nextState);
  history.length = globalHistory.length;  // 通知订阅者,history已更新。控制react组件重新渲染的关键就在这里  transitionManager.notifyListeners(history.location, history.action);}

其实,在Router组件初始化的时候,就监听了history的更新,下面是Router组件的代码:


...  // 这里的history.listen是history对象提供的方法,用于监听页面history的更新。  _this.unlisten = props.history.listen(function (location) {    if (_this._isMounted) {      _this.setState({        location: location      });    } else {      _this._pendingLocation = location;    }  });
...

可以看到,Router组件监听了history的更新。当页面的history更新时,会调用Router组件的setState,从而完成页面的re-render。


总结


hashHistory的逻辑与BrowserHistory的逻辑类似,本文就不再继续展开了,

到这里,可以简单总结一下,整个react-router的实现思路是:

使用一个第三方的、框架无关的history对象来控制页面的路由变化逻辑。在react侧,使用context来传递history对象,保证路由组件中可以访问到history对象、方便控制路由,并且将history对象与业务组件隔离。使用发布订阅模式,解耦了页面路由的更新与react的更新。

在我们自己的业务组件中,无法直接访问到history对象。如果想直接访问到history对象,可以使用withRouter这个HOC。


写在后面


前端路由系列文章算是告一段落了。本系列文章从最基本的路由原理讲起,到框架的路由实现结束,还算符合预期。不过路由中还涉及到不少的知识点 以及一些高级的功能(比如 keep-alive),值得继续研究

相关文章
|
2月前
|
前端开发 JavaScript
React 步骤条组件 Stepper 深入解析与常见问题
步骤条组件是构建多步骤表单或流程时的有力工具,帮助用户了解进度并导航。本文介绍了在React中实现简单步骤条的方法,包括基本结构、状态管理、样式处理及常见问题解决策略,如状态管理库的使用、自定义Hook的提取和CSS Modules的应用,以确保组件的健壮性和可维护性。
83 17
|
16天前
|
前端开发 Java Shell
【08】flutter完成屏幕适配-重建Android,增加GetX路由,屏幕适配,基础导航栏-多版本SDK以及gradle造成的关于fvm的使用(flutter version manage)-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
【08】flutter完成屏幕适配-重建Android,增加GetX路由,屏幕适配,基础导航栏-多版本SDK以及gradle造成的关于fvm的使用(flutter version manage)-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
121 20
【08】flutter完成屏幕适配-重建Android,增加GetX路由,屏幕适配,基础导航栏-多版本SDK以及gradle造成的关于fvm的使用(flutter version manage)-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
|
1月前
|
机器学习/深度学习 人工智能 自然语言处理
DeepSeek Artifacts:在线实时预览的前端 AI 编程工具,基于DeepSeek V3快速生成React App
DeepSeek Artifacts是Hugging Face推出的免费AI编程工具,基于DeepSeek V3,支持快速生成React和Tailwind CSS代码,适合快速原型开发和前端组件构建。
929 39
DeepSeek Artifacts:在线实时预览的前端 AI 编程工具,基于DeepSeek V3快速生成React App
|
5天前
|
Web App开发 监控 前端开发
React音频播放控制组件开发深度解析
本文介绍了构建React音频控制组件时遇到的关键问题及优化方案。主要包括: 1. **状态同步难题**:解决播放按钮与音频状态不同步的问题,通过双向绑定机制确保一致。 2. **跨浏览器兼容性**:处理Safari和Chrome预加载策略差异,确保`duration`属性正确获取。 3. **进度控制优化**:避免使用`setInterval`,采用`requestAnimationFrame`提升性能;优化拖拽交互,防止音频卡顿。 4. **音量控制进阶**:实现渐变音量调节和静音状态同步。
50 15
|
3月前
|
监控 前端开发 数据可视化
3D架构图软件 iCraft Editor 正式发布 @icraft/player-react 前端组件, 轻松嵌入3D架构图到您的项目,实现数字孪生
@icraft/player-react 是 iCraft Editor 推出的 React 组件库,旨在简化3D数字孪生场景的前端集成。它支持零配置快速接入、自定义插件、丰富的事件和方法、动画控制及实时数据接入,帮助开发者轻松实现3D场景与React项目的无缝融合。
280 8
3D架构图软件 iCraft Editor 正式发布 @icraft/player-react 前端组件, 轻松嵌入3D架构图到您的项目,实现数字孪生
|
2月前
|
前端开发 UED
React 文本区域组件 Textarea:深入解析与优化
本文介绍了 React 中 Textarea 组件的基础用法、常见问题及优化方法,包括状态绑定、初始值设置、样式自定义、性能优化和跨浏览器兼容性处理,并提供了代码案例。
102 8
|
2月前
|
移动开发 缓存 前端开发
深入理解前端路由:原理、实现与应用
本书《深入理解前端路由:原理、实现与应用》全面解析了前端路由的核心概念、工作原理及其实现方法,结合实际案例探讨了其在现代Web应用中的广泛应用,适合前端开发者和相关技术人员阅读。
|
3月前
|
负载均衡 网络协议 定位技术
在数字化时代,利用DNS实现地理位置路由成为提升用户体验的有效策略
在数字化时代,利用DNS实现地理位置路由成为提升用户体验的有效策略。通过解析用户请求的来源IP地址,DNS服务器可判断其地理位置,并返回最近或最合适的服务器IP,从而优化网络路由,减少延迟,提高访问速度。示例代码展示了如何基于IP地址判断地理位置并分配相应服务器IP,实际应用中需结合专业地理数据库和动态调整机制,以应对复杂网络环境带来的挑战。
64 6
|
3月前
|
缓存 前端开发 JavaScript
JavaScript前端路由的实现原理及其在单页应用中的重要性,涵盖前端路由概念、基本原理、常见实现方式
本文深入解析了JavaScript前端路由的实现原理及其在单页应用中的重要性,涵盖前端路由概念、基本原理、常见实现方式(Hash路由和History路由)、优点及挑战,并通过实际案例分析,帮助开发者更好地理解和应用这一关键技术,提升用户体验。
128 1
|
3月前
|
前端开发 JavaScript 算法
探索现代前端框架——React 的性能优化策略
探索现代前端框架——React 的性能优化策略
44 0

热门文章

最新文章

  • 1
    【11】flutter进行了聊天页面的开发-增加了即时通讯聊天的整体页面和组件-切换-朋友-陌生人-vip开通详细页面-即时通讯sdk准备-直播sdk准备-即时通讯有无UI集成的区别介绍-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
  • 2
    【08】flutter完成屏幕适配-重建Android,增加GetX路由,屏幕适配,基础导航栏-多版本SDK以及gradle造成的关于fvm的使用(flutter version manage)-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
  • 3
    【05】flutter完成注册页面完善样式bug-增加自定义可复用组件widgets-严格规划文件和目录结构-规范入口文件-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈
  • 4
    详解智能编码在前端研发的创新应用
  • 5
    巧用通义灵码,提升前端研发效率
  • 6
    智能编码在前端研发的创新应用
  • 7
    【04】flutter补打包流程的签名过程-APP安卓调试配置-结构化项目目录-完善注册相关页面-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程
  • 8
    【07】flutter完成主页-完成底部菜单栏并且做自定义组件-完整短视频仿抖音上下滑动页面-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈
  • 9
    抛弃node和vscode,如何用记事本开发出一个完整的vue前端项目
  • 10
    大前端之前端开发接口测试工具postman的使用方法-简单get接口请求测试的使用方法-简单教学一看就会-以实际例子来说明-优雅草卓伊凡
  • 推荐镜像

    更多