react router component与render有什么区别?提升渲染性能,记一个react router component 误用导致请求死循环的有趣bug

壹 ❀ 引
下午前端大佬突然私聊我,说发现了一个很有趣的bug
,问我有没有兴趣,因为我平时会记录一些自认为有意思的问题,所以毫不犹豫就答应了,问题表现如下,当我们系统进入到某个页面下时,接口居然无止境的不断请求,跟陷入了死循环一样。

问题简单排查下来其实也不算复杂,算是react router
理解不够深刻使用不当造成的问题,处于好奇在项目里搜了下这种不当写法,统计来看应该有不少同学对于这块也不太熟悉,所以这里就做个简单记录。
贰 ❀ 排查思路
因为接口在不断请求,我们自然要排查这个接口是谁发起的,从而定位出发请求的问题组件。点击上图中的data
接口,选择Initiator
,在这里我们就能看到这个接口从发起到结束整个完整的调用栈,因为我是点击这个页面就出现这个问题,说明这个数据极大可能是在页面初始化的请求,初始化请求一般放在哪?当然是componentDidMout
,于是我们查找调用找中的componentDidMout
,于是成功定位到了如下文件:

点击文件,可以看到具体的代码确实是在初始化拿数据:

那么问题来了,组件渲染理论上只会执行一次componentDidMount
,如果它一直在卸载挂载,那说明出问题的不是组件自身,而是使用了此组件的上游组件,于是我拿这个组件名在项目里搜索了一番,运气还算好,只有一个路由页用到,大致代码如下:
<Route component={() => <A {...this.props} />} />
而这种写法,其实就引发了一个很尴尬的问题,打开react router
官方,有如下这段描述:
When you use
component
(instead ofrender
orchildren
, below) the router usesReact.createElement
to create a new React element from the given component. That means if you provide an inline function to thecomponent
prop, you would create a new component every render. This results in the existing component unmounting and the new component mounting instead of just updating the existing component. When using an inline function for inline rendering, use therender
or thechildren
prop (below).
总结来说,如果我们使用了component
,路由会使用React.createElement
帮你创建一个新的react
组件,而且是卸载现有组件以及挂载你设置的新组件,但是上述写法使用了箭头函数,导致只要路由这段代码render
执行一次,即便路由地址没发生变化,component
都会认定这是一个新组件,从而每次都完整执行生命周期钩子,那写在didMount
中的请求自然每次都会请求。
那为啥包含路由相关代码逻辑的父组件一直在render
呢?这里需要提一提我们项目中所使用的stamp
接口机制,前端每次拿数据,除了告诉后端要拿什么数据之外,都会附带一个时间戳。

比如第一次请求前端时间戳带过去的肯定是0,后端返回了数据以及一个此数据对应的时间戳;
第二次再请求时,前端会带上上次后端给的时间戳与后端做对比,假设数据没变化,后端对于数据层就会返回null
以及还是相同的时间戳,前端接到null
自然知道数据没变化了,还是走缓存,甚至组件都不会有更新的必要。
但这个问题巧就巧在后端在数据层返回出了问题,带了数据但是没给时间戳,导致前端每次请求的时间戳都是默认的0,从而后端每次都返回新数据,新数据被存入store
,数据引用发生变化导致路由所在组件渲染,路由渲染又引发下层组件渲染以及didMout
执行,于是请求死循环就诞生了。

虽然后端数据返回有问题是前提,但是前端也不应该发起无意义的请求,说到底就是不应该重复的didMout
,怎么修改呢?将component
改为render
即可:
<Route render={() => <A {...this.props} />} />
叁 ❀ 一个例子加深印象
为了更好的理解上述问题,以及component
与render
的使用对比,这里我准备了一个例子,先看component
:
class B extends React.Component {
componentDidMount() {
// 用于判断子组件didMout是否重复执行
console.log("componentDidMount")
}
render() {
return (
<div>B组件,此时num是{this.props.num}</div>
)
}
}
class Echo extends React.Component {
state = { num: 1 }
componentDidMount() {
// 定时器,模拟后端不断返回新数据,引发render变化
setInterval(() => {
this.setState({ num: this.state.num + 1 });
}, 1000)
}
render() {
return (
<div>
<BrowserRouter>
<Route component={() => (<B num={this.state.num} />)} />
</BrowserRouter>
</div>
);
}
}

可以看到组件B不仅render
在不断执行,连componentDidMout
也在不断执行,若大家有兴趣,可以再给B组件嵌套一个C组件,同时也监听componentDidMout
,你会发现component
所接收组件下的整个组件树,都在完整的被重新卸载挂载,抛开本文提到的请求死循环,单站在react
角度性能也存在一定问题。
现在我们将上述代码中的component
改成render
,效果如下,可以看到子组件正常渲染,且componentDidMout
只会初始化一次,后续不会重复执行。

肆 ❀ 总结
我们在路由写法上,常见写法如下:
<Route component={B} />
但是上述写法并没办法传递props
以及其它属性,所以有同学可能就习惯使用箭头函数的做法,如下:
<Route component={() => <B {...this.props} />} />
但是我们通过一个bug
分析,以及例子演示得知,假设当前路由未变化但是触发了render
,这些用法会导致路由下子组件完整的重复挂载卸载,非常影响性能,解决办法也很简单,改用render
即可。
那么component
与render
又有什么区别呢?这里我去简单看了下路由源码:
// react-router源码
if (component)
return match ? React.createElement(component, props) : null
if (render)
return match ? render(props) : null
源码层面,创建组件的方式不同,component 使用的是 React.createElement,箭头函数情况下由于每次返回的都是一个新组件,所以每次都会触发完整的生命周期;而 render 可以理解执行了一个匿名函数,得到了一个组件,自始至终都是这一个组件,后续更新只是diff比较,就没有额外繁琐的生命周期处理,性能更佳。
对于component
,它的调用更像下面代码:
<Route component={B} />
// 你可以理解为
<Route>
<B />
</Route>
而对于使用render
的场景,它更像下方这样:
<Route>
{B()}
</Route>
那么到这里,本文结束。
react router component与render有什么区别?提升渲染性能,记一个react router component 误用导致请求死循环的有趣bug的更多相关文章
- React爬坑秘籍(一)——提升渲染性能
React爬坑秘籍(一)--提升渲染性能 ##前言 来到腾讯实习后,有幸八月份开始了腾讯办公助手PC端的开发.因为办公助手主推的是移动端,所以导师也是大胆的让我们实习生来技术选型并开发,他来做code ...
- 记一个react拖动排序中的坑:key
在做一个基于react的应用的时候遇到了对列表拖动排序的需求.当使用sortable对列表添加排序支持后发现一个问题:数据正确排序了,但是dom的顺序却乱了,找了一会儿原因后发现是因为在渲染数据的时候 ...
- react+redux渲染性能优化原理
大家都知道,react的一个痛点就是非父子关系的组件之间的通信,其官方文档对此也并不避讳: For communication between two components that don't ha ...
- 从性能角度看react组件拆分的重要性
React是一个UI层面的库,它采用虚拟DOM技术减少Javascript与真正DOM的交互,提升了前端性能:采用单向数据流机制,父组件通过props将数据传递给子组件,这样让数据流向一目了然.一旦组 ...
- [React Router v4] Conditionally Render a Route with the Switch Component
We often want to render a Route conditionally within our application. In React Router v4, the Route ...
- React源码 ReactDOM.render
在 react 当中,主要创建更新的有三种方式 1.ReactDOM.render || hydrate 这两个api都是要把这个应用第一次渲染到我们页面上面,展现出来整个应用的样子的过程,这就是初 ...
- [React] Use React.ReactNode for the children prop in React TypeScript components and Render Props
Because @types/react has to expose all its internal types, there can be a lot of confusion over how ...
- nuxt框架Universal和Spa两种render mode的区别
如下图,官网上对于Universal 和 Spa 两种render mode的区别,并没有加以说明,相信大多数人跟我一样有点懵,不知道选什么好.虽然两个模式创建的项目看不出区别. 先给出推荐选项: U ...
- ElementUI(vue UI库)、iView(vue UI库)、ant design(react UI库)中组件的区别
ElementUI(vue UI库).iView(vue UI库).ant design(react UI库)中组件的区别: 事项 ElementUI iView ant design 全局加载进度条 ...
- react route使用HashRouter和BrowserRouter的区别-Content Security Policy img-src 404(Not found)
踩坑经历 昨天看了篇关于react-route的文章,说BrowserRouter比HashRouter好一些,react也是推荐使用BrowserRouter,毕竟自己在前端方面来说,就是个小白,别 ...
随机推荐
- 小技巧:WIndows快速创建文件夹
快速创建文件夹的技巧 1.首先创建文本文档将扩展名更改为.bt,mkdir.bat 2.写入创建文件夹的代码 md 文件夹1 文件夹2 文件夹3 pause 3.双击执行mkdir.bat
- [kubernetes]安装dashboard
前言 kubernetes官方文档中的web UI网页管理工具是kubernetes-dashboard,可提供部署应用.资源对象管理.容器日志查询.系统监控等常用的集群管理功能.为了在页面上显示系统 ...
- 【C++】枚举作为类函数返回值时需定义在使用之前
枚举定义在前,作为函数返回值在后 枚举定义在后,则函数返回值需用普通类型
- 在TypeScript项目中搭配Axios封装后端接口调用
前言 本来是想发 next.js 开发笔记的,结果发现里面涉及了太多东西,还是拆分出来发吧~ 本文记录一下在 TypeScript 项目里封装 axios 的过程,之前在开发 StarBlog-Adm ...
- [转帖]【TiDB】快速起步
1. 存储引擎的的功能 提供数据存储接口并持久化存储数据 2. LSM-tree 的特性 LSM-tree 结构本质上是一个用空间置换写入延迟,用顺序写入替换随机写入的数据结构 3. 数据库技术的发展 ...
- [转帖]TiKV读写流程浅析
https://www.cnblogs.com/luohaixian/p/15227838.html 1.TiKV框架图和模块说明 图1 TiKV整体架构图 1.1.各模块说明 PD Cluster ...
- [转帖]Redis核心技术与实战
https://www.cnblogs.com/strick/p/14851429.html 最近在读一篇关于Redis的专栏,叫做<Redis核心技术与实战>,作者在Redis方面研究颇 ...
- 【转帖】【ethtool】ethtool 网卡诊断、调整工具、网卡性能优化| 解决丢包严重
目录 即看即用 详细信息 软件简介 安装 ethtool的使用 输出详解 其他指令 将 ethtool 设置永久保存 如何使用 ethtool 优化 Linux 虚拟机网卡性能 ethtool 解决网 ...
- [转帖]springboot指定端口的三种方式
https://blog.51cto.com/feirenraoyuan/5504099 第一配置文件中添加server.port=9090 第二在命令行中指定启动端口,比如传入参数 java -ja ...
- 转载:ubuntu各个版本的发行时间和停止支持的时间,更新到最新版和代号。
版本:20.10 代号:Groovy Gorilla 发布时间:2020/10/22 版本:20.04 LTS 代号:Focal Fossa 发布时间:2020/4/23 版本:19.10 ...