起因

目前负责的项目中有一个微信网页,用的是react技术栈。在该项目中增加了一个微信分享功能后,线上ios出现了问题,经排查,定位到了react的路由系统。

这次线上bug,让我决定,先从react-router-dom开始,看看它内部实现了什么。

前端目前用到的就是react-router-dom这个库,它提供了两个高级路由器,分别是BrowserRouterHashRouter,它两的区别就是一个用的history API ,一个是使用URL的hash部分,接下来我以BrowserRouter为例,做一个解读。

简易过程图

解读(只摘取核心代码进行展示)

首先看看整个react-router-dom提供了点啥?

export {MemoryRouter,Prompt,Redirect,Route,Router,StaticRouter,Switch,generatePath,matchPath,withRouter,useHistory,useLocation,useParams,useRouteMatch
} from "react-router";export { default as BrowserRouter } from "./BrowserRouter.js";
export { default as HashRouter } from "./HashRouter.js";
export { default as Link } from "./Link.js";
export { default as NavLink } from "./NavLink.js";

除了下面它自己实现的四个组件外,其余的都是将react-router提供的组件做了一个引入再导出,那看来底层核心的东西还是在react-router上。

1.先从一个简单demo开始

import { BrowserRouter, Route, Switch, Link } from "react-router-dom"function App() {return (<BrowserRouter><div>主菜单</div><Link to="/home">home</Link><br /><Link to="/search">search</Link><hr /><Switch> <Route path="/home" component={Home}  /><Route path="/search" component={Search} /></Switch></BrowserRouter>)
}ReactDOM.render(<App />, document.getElementById('root'));

需要通过路由跳转来实现UI变化的组件,要用BrowserRouter作为一个根组件来包裹起来,Route用来盛放页面级的组件。

那按照这种层级关系,我们先来看下BrowersRouter里实现了什么功能。

2. BrowersRouter

import { Router } from "react-router";
import { createBrowserHistory as createHistory } from "history";class BrowserRouter extends React.Component {history = createHistory(this.props);render() {return <Router history={this.history} children={this.props.children} />;}
}

非常少量的几行代码,很清晰的看到,核心点是history这个库所提供的函数。组件在render前执行了createHistory这个函数,然后它会返回一个history的对象实例,然后通过props传给Router这个路由器,另外其中包裹的所有子组件,统统传给Router。

这里其实官网上已经说的很清楚,大家用的时候可以多留意下。

那么思路就很清楚,重点放在Router和history库上,看看Router是怎么用这个history对象的,以及这个history对象里又包含了啥,和window.history有什么区别?让我们接着往下走。

3. Router

import HistoryContext from "./HistoryContext.js";
import RouterContext from "./RouterContext.js";

Router是核心的路由器,上面我们已经看到BrowsRouter传递给它了一个history对象。

首先引入了两个context,这里其实就是创建的普通context,只不过拥有特定的名称而已。

它的内部实现是这样

const createNamedContext = name => {const context = createContext();context.displayName = name;return context;
};// 上述的引用就相当于 HistoryContext = createNamedContext("Router-History")

引入了这两个context后,在来看它的构造函数。

constructor(props) {super(props);this.state = {location: props.history.location};this.unlisten = props.history.listen(location => {this.setState({ location });});}

Router组件维护了一个内部状态location对象,初始值为上面提到的在BrowsRouter中创建的history提供的。

之后,执行了history对象提供的listen函数,这个函数需要一个回调函数作为入参,传入的回调函数的功能就是来更新当前Router内部状态中的location的,关于什么时候会执行这个回调,以及listen函数,后面会详细剖析。

 componentWillUnmount() {if (this.unlisten) {this.unlisten();}}

等这个Router组件将要卸载时,就取消对history的监听。

 render() {return (<RouterContext.Providervalue={{history: this.props.history,location: this.state.location,match: Router.computeRootMatch(this.state.location.pathname),staticContext: this.props.staticContext}}> <HistoryContext.Providerchildren={this.props.children || null}value={this.props.history}/> </RouterContext.Provider>);}

最后生成的react树,就是由最开始引入的context组成的,然后传入history、location这些值。

总结就是整个Router就是一个传入了history、locaiton和其它一些数据的context的提供者,然后它的子组件作为消费者就可以共享使用这些数据,来完成后面的路由跳转、UI更新等动作。

3. histroy库

在Router组件可以看到已经用到了createBrowserHistory函数返回的history实例了,如:history.location和history.listen,这个库里的封装的函数那是相当多了,细节也很多,我仍然挑最重要的解读。

首先是咱们这个出镜率较高的history提供了哪些属性和方法

看起来都是些熟悉的东西,如push、replace、go这些,都是window对象属性history所提供的。但有些属性其实是重写了的,如push、replace,其它的是做了一个简单封装。

  function goBack() {go(-1);}function goForward() {go(1);}

Router内部状态location的初始数据,是使用window.location与window.history.state做的重组。

路由系统最为重要的两个切换页面动作,一个是push,一个是replace,我们平时只用Link组件的话,并没有确切的感受,其中Link接受一个props属性,to :string 或者to : object

<link to='/course'>跳转</link>

此时点击它时,调用的就是props.history中重写的push方法。

<Link to='/course' replace>跳转</Link>

如果增加replace属性,则用的就是replace方法

这两个方法主要用的是pushStatereplaceState这两个API,它们提供的能力就是可以增加新的window.history中的历史记录和浏览器地址栏上的url,但是又不会发起真正的网络请求。

这是实现单页面应用的关键点。

然后让我们看一下这两个路由跳转方法

精简后,代码还是不少,我解读下。

push中的入参path,是接下来准备要跳转的路由地址。createLocation方法先将这个path,与当前的location做一个合并,返回一个更新的loation。

然后就是重头戏,transitionManager这个对象,让我们先关注下成功回调里面的内容。

通过更新后的location,创建出将要跳转的href,然后调用pushState方法,来更新window.history中的历史记录。

如果你在BrowserRouter中传了forceRefresh这个属性,那么之后就会直接修改window.lcoation.href,来实现页面跳转,但这样就相当于要重新刷新来进行网络请求你的文件资源了。

如果没有传的话,就是调用setState这个函数,注意这个setState并不是react提供的那个,而是history库自己实现的。

function setState(nextState) {history.length = globalHistory.length;transitionManager.notifyListeners(history.location, history.action);}

还是用到了transitionManager对象的一个方法。

另外当我们执行了pushState后,接下来所获取到的window.history都是已经更新的了。

接下来就剩transitionManager这最后的一个点了。

transitionManager是通过createTransitionManager这个函数实例出的一个对象

  function createTransitionManager() {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;});};}function notifyListeners() {for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {args[_key] = arguments[_key];}listeners.forEach(function (listener) {return listener.apply(void 0, args);});}return {    appendListener: appendListener,    notifyListeners: notifyListeners  };}

还记的开始时我们在Router组件中已经用过一个history.listen方法,其中内部实现就是用的transitionManager.appendListener方法

function listen(listener) {var unlisten = transitionManager.appendListener(listener);checkDOMListeners(1);return function () {checkDOMListeners(-1);unlisten();};}

当时我们给listen传入了一个回调函数,这个回调函数是用来通过React的setState来更新组件内部状态的locaton数据,然后又因为这个lcoation传入了Router-context的value中,所以当它发生变化时,所有的消费组件,都会重新render,以此来达到更新UI的目的。

listen的执行细节是,把它的入参函数(这里指更新Rrouter的state.location的函数)会传入到appendListener中。

执行appendListener后,appendListener将这个入参函数推到listeners这个数组中,保存起来。然后返回一个函数用来删除掉推进该数组的那个函数,以此来实现取消监听的功能。

所以当我们使用push,切换路由时,它会执行notifyListeners并传入更新的location。

然后就是遍历listeners,执行我们在listen传入的回调,此时就是最终的去更新Router的location的过程了。

后面的流程,简单说下,Router里面的Route组件通过匹配pathname 和 更新的location ,来决定是否渲染该页面组件,到此整个的路由跳转的过程就结束了。

总结

第一次阅读源码,尽管删减了很多,但还是写了不少。

希望大家可以沿着这个思路,自己也去看看,还是有很多细节值得推敲的。

react封装函数_React-Router源码解读相关推荐

  1. usestate中的回调函数_React Hooks 源码解析(3):useState

    React 源码版本: v16.11.0 源码注释笔记: airingursb/react​github.com 在写本文之前,事先阅读了网上了一些文章,关于 Hooks 的源码解析要么过于浅显.要么 ...

  2. react封装函数_react request.js 函数封装

    1.request.js  函数封装 import { Toast } from 'antd-mobile'; import axios from 'axios'; import store from ...

  3. react封装函数_React 模式-将函数作为 children 传入和 render prop - 极客教程

    最近几个月来,React 社区开始转向一个有趣的方向.到目前为止,我们的示例中的 children 属性都是 React 组件.然而,有一种新的模式越来越受欢迎,children 属性是一个 JSX ...

  4. react封装函数_React中函数式声明组件

    前文介绍的组件的定义方式主要是声明式组件,其与传统的jQuery中以DOM操作为核心的命令式组件生成相比具有更大的灵活性与可组合性.而实际上随着应用复杂度与所需要的组件数目的持续增加,我们所需要的组件 ...

  5. Alamofire源码解读系列(九)之响应封装(Response)

    本篇主要带来Alamofire中Response的解读 前言 在每篇文章的前言部分,我都会把我认为的本篇最重要的内容提前讲一下.我更想同大家分享这些顶级框架在设计和编码层次究竟有哪些过人的地方?当然, ...

  6. Alamofire源码解读系列(五)之结果封装(Result)

    本篇讲解Result的封装 前言 有时候,我们会根据现实中的事物来对程序中的某个业务关系进行抽象,这句话很难理解.在Alamofire中,使用Response来描述请求后的结果.我们都知道Alamof ...

  7. React 之 Refs 的使用和 forwardRef 的源码解读

    三种使用方式 React 提供了 Refs,帮助我们访问 DOM 节点或在 render 方法中创建的 React 元素. React 提供了三种使用 Ref 的方式: 1. String Refs ...

  8. EVM源码解读(1):amplify_spatial_lpyr_temporal_iir函数

    前言 本文是对MIT多媒体实验室论文<Eulerian video magnification for revealing subtle changes in the world>对应代码 ...

  9. PostgreSQL 源码解读(147)- Storage Manager#3(fsm_search函数)

    本节简单介绍了PostgreSQL在执行插入过程中与存储相关的函数RecordAndGetPageWithFreeSpace->fsm_search,该函数搜索FSM,找到有足够空闲空间(min ...

  10. dva处理_dva 源码解读

    声明 本文章用于个人学习研究,并不代表 dva 团队的任何观点. 原文以及包含一定注释的代码见这里,若有问题也可以在这里进行讨论 起步 为什么是dva? 笔者对 dva 的源代码进行解读,主要考虑到 ...

最新文章

  1. AI 机器人也能成佛?日本人觉得可以。
  2. Cisco IOS版本命名规范
  3. 几句话描述简单算法——排序与搜索
  4. r语言上机文本分析与词云绘制_倚天屠龙记的文本分析
  5. android的交互方式,Android与js的交互方式
  6. redis hash field过期时间_大佬来告诉你用事半功倍的办法,学习Redis,你觉得它还难吗?...
  7. JS CKEditor使用setData后绑定click事件
  8. 我的世界java种子 要塞,我的世界:稀奇种子,恐龙骨架出现在要塞,你绝对没见过...
  9. 【读书】卡勒德·胡赛尼《群山回唱》 摘录
  10. gentoo的安装坑
  11. 中国科技大学计算机系导师,中国科学技术大学
  12. 主wifi旁零距离添加AP路由器
  13. 每日一练——回文链表
  14. ShareSDK 抖音平台注册
  15. matlab 进行非线性回归,5.利用Matlab编程进行非线性回归分析.doc
  16. Linux 性能分析命令详解
  17. 上海迪士尼将新增黄色小狗可琦安系列主题商品
  18. 一个进入保护模式加载引导程序的BOOTLOADER
  19. Leap Motion开发(六)多Leap Motion研究
  20. element-UI el-tree横向滚动条

热门文章

  1. Node工程-构建优秀的Session机制
  2. Introduction of Open CASCADE Foundation Classes
  3. Centos7-install apache+mariadb+php
  4. python中的matplotlib(1)
  5. ADB 操作手机的粘贴板
  6. 百度数据可视化图表套件echart实战
  7. js window.open()实现打印,如何在关闭打印窗口时刷新父窗口
  8. 百度技术研发笔试题目
  9. Geography爱好者 QGIS WGS84转其它坐标系并计算坐标
  10. 写1行代码影响1000000000人,这是个什么项目?