壹 ❀ 引

因为现在工作主要以修bug为主,日常工作中总是会接触到千奇百怪的前端问题,它可能是代码缺陷导致的程序错误,也可能是方案不合理造成的性能问题,老实说修bug是一件很枯燥的事情,你需要阅读大量陌生的代码去阅读问题,并不断缩小问题范围找出根因,同时还要保证在修复过程中不会产生额外的其它问题(确认影响范围)。

但反过来想,修复的每一个问题对自己也有一定的警示作用,我是否会写出这样的代码,又该如何避免犯同样的错误?修复bug也算是一种代码经验的积累。那么这篇文章,会记录一个因为同事react组件书写不规范造成的bug,问题排查2小时,修复仅需要改一行代码,那么本文正式开始。

贰 ❀ 场景重现与排查思路

某天下午还在打哈欠,CSM提了一个问题单反馈了一个bug,说项目在使用过程中遇到了一个问题,工作项任务改状态(比如未开始改为已完成),本来应该有个弹窗弹出(弹窗作用是可以配置一些状态修改后置动作,比如登记工时,添加评论之类的),但问题在于现在第一次改状态并不会有弹窗,非要改第二次才会正常弹出,希望我这边排查解决下问题,bug单上也附带了问题表现的视频。

排查问题第一步是得复现问题,于是我也做了与视频中相同的状态修改后置动作配置,很顺利的就复现了问题,而且打开控制台,发现第一次修改会有控制台错误,错误如下:

这个错误啥意思呢,其实就是对象使用拓展运算符结构失败了,我们可以在控制台复制并运行如下代码,就可以复现这个错误:

let obj = {name:'echo'};
console.log(...obj)

原因也很简单,对象在结构时,得用{}包裹,比如:

let {name,...rest} = {name:'echo',age:18};
name //echo
rest //{age:18}

于是根据错误顺利定位到报错的代码,如下:

正如我们所推测的那样,此时的rest明显是一个对象,直接这样解构肯定会报错,难道是这里的代码书写错误导致?打开项目代码,搜索定位发现,报错代码所在的文件已经四年无人改动了....即便从现象上看是这里的代码错误,但此组件还是不要修改为妙,站在修复影响范围来说,这个bug以前没出现但现在出现,更大概率是上层使用者不当所导致。

回到报错的代码,注意上图中执行这段代码的判断条件,此时datanull,前面问题也说了,第一次点击报错,第二次点击正常,那么正常情况下这里会怎么执行呢?于是我又断点查看正常情况下的代码执行,如下图:

当第二次点击改状态,发现此时的data是一个空数组,因为!datafalse所以没进入执行这段有问题的代码,弹窗才能顺利弹出,那么问题就来了,这个data从何而来。于是我又阅读了当前报错的组件对于props的定义,发现代码中确实为当前组件定义了data默认值,只是这个默认值是null

那么现在我们可以大胆猜测,第一点击行为要么上层组件传递了dataundefined,要么没传data,这才导致了组件data取了默认值,而第二次点击因为data传递了[],所以没报错。现在确认了问题来自上层组件数据传递问题,要做的就是数据溯源,进一步缩小问题范围。

其实在CSM提单的时候,我当时还没看工单中的复现视频,而是自己直接去复现了,结果发现复现不了。之后才去看视频,把后置动作配置改成和视频中一模一样才成功复现。

而这个配置,一共有文件、关联wiki页面、评论这三个属性,评论因为是默认配置,那么问题是文件或者关联wiki页面引发,经过测试发现,原来只有添加了关联wiki页面这个属性才会报错,既然确认了组件,那就直接打开chrome react组件插件对相关组件props传递进行检查,最终顺利定位一个名为wikiPages的字段(这是功能正常情况下的属性截图):

大概梳理了下逻辑,上层组件负责查询找到已关联wiki的相关wiki页面ID,然后通过wikiPages字段传递给关联wiki这个组件,因此假设用户之前没有关联wiki,那么这个数组应该是个空数组。可问题就出在,为什么第一点击时,这个字段没传递(或者传递了undefined),导致底层组件使用了默认值null从而报错。

于是我又阅读了查找关联wiki ID以及出数据传递的逻辑,终于明白为什么会出现这个问题,下面是大概的代码结构:

// 修改完状态后,应该展示出的弹窗组件
class XXXDialog extends React.PureComponent {
// 定义了state
state = {
wikiPages: this.props.relatedWikiList
}; renderRelatedWiki = (transitionField) => (
<RelatedWiki
// 在这里使用了state中的wikiPages字段,传递了关联wiki组件
relatedWikiList={this.state.wikiPages}
/>
);
} const mapStateToProps = () => {
// 查找到已关联wiki的列表数据
const relatedWikiList = memoize();
return {
relatedWikiList
}
}

简单来说,代码作者在mapStateToProps中准备好了relatedWikiList数据并通过props传递给当前组件,因此在state定义时,作者直接使用wikiPages: this.props.relatedWikiList来做wikiPages初始化,同时,在渲染下层组件时,又使用relatedWikiList={this.state.wikiPages}做了数据传递。

那么这么写有个什么问题呢?我们知道,对于react组件声明周期而言,constrcutor是一定先于mapStateToProps执行,那么组件初次渲染时,此时this.props.relatedWikiList这个数据都还没准备好,因此上述代码state的初始化等同于:

state = {
wikiPages: undefined
};

所以关联wiki这个组件的relatedWikiList属性一开始传递的是undefined,而对于react而言,假设一个字段传递undefined等同于没传递,那么组件有默认值肯定就会用默认值,这也解释了为什么第一次点击datanull,导致了程序报错。

而上述代码其实也在componentWillReceiveProps函数中感知了relatedWikiList变化用于更新state,但bug这个东西永远是超出你预期它才会产生bug,因为底层组件渲染直接遇到代码错误,导致整个生命周期并未能再次循环,这个componentWillReceiveProps根本就没机会执行,也没能成功修改state引起第二次组件渲染。

当第二次点击修改状态时,此时mapStateToProps执行了(可能是store或者外部props变化了),终于传递了一个数组格式的数据给了当前组件,这也是为什么第二次能弹出弹窗的原因。

关于生命周期顺序的问题,可以看下面这个小例子:

import React, {
Component
} from 'react';
import ReactDOM from 'react-dom';
import { connect } from 'react-redux' class Parent extends Component {
constructor(props) {
super(props)
console.log('执行了constructor');
this.state = {
name: this.props.name
}
}
render() {
console.log(this.state.name)
return (
<div>{this.state.name}</div>
);
}
} function mapStateToProps(state, ownProps) {
const name = 'echo';
console.log('执行了mapStateToProps')
return {
name
}
} connect(mapStateToProps)(Parent);
ReactDOM.render(
<Parent />,
document.getElementById('root')
);

这个例子中,mapStateToProps未能成功执行,是因为stateownProps没有改变,所以未能触发此方法执行,但大致上我们还是证明了之前推测,组件永远都是自己先渲染一遍,此后因为store或者外部props发生改变后再次触发组件渲染。

回到问题本身,我们前面也说了,当关联wiki没有数据时,本身预期的就是一个空数组,而因为不合规的代码书写,导致了关联wiki默认值成了undefined,怎么修改呢?其实就一句的修改,将state初始化改为:

state = {
wikiPages: []
};

刷新页面,问题顺利解决,总结来说,这是一个对于react声明周期不熟悉,以及不太友好的代码书写造成的bug,排查两小时,修改一个词。另外再次强调,初始化state不要通过this.props这种方式赋值,因为你无法预估它的默认值是什么,以及下层组件会怎么使用,大概如此了。

叁 ❀ 总

老实说,修bug的工作确实很像侦破一个案件,我需要搜集有限的信息,排查,还原现场,组合各种情况不断去缩小范围,最终确认问题点,并给出影响范围最小的修改方案,这个过程有点枯燥,也有点有趣。之后应该也会不间断更新一些奇怪bug的排查思路,给自己排坑,避免自己也写出类似的代码,那么本文结束。

【React】排查两小时,修改一个词,记一个因代码书写不规范导致的生命周期BUG的更多相关文章

  1. 一个例子说明Jsp三大重要内置对象的生命周期

    此处Jsp的三大内置对象指:request,session以及application.他们共有的方法:setAttribute,getAttribute,方法名和方法作用都是相同的,但是作用范围不一样 ...

  2. 【codevs2216】行星序列 线段树 区间两异同修改+区间求和*****

    [codevs2216]行星序列 2014年2月22日3501 题目描述 Description “神州“载人飞船的发射成功让小可可非常激动,他立志长大后要成为一名宇航员假期一始,他就报名参加了“小小 ...

  3. 通俗易懂了解React生命周期

    1.前言 学习React时,学习组件的生命周期是非常重要的,了解了组件的"从无到有再到无"所经历的各个状态,对日后写高性能的组件会有很大的帮助. 2.生命周期图 React的生命周 ...

  4. 【Android开发-8】生命周期,Activity中打开另外一个Activity

    前言:生命中有很多人陪伴自己走过一生中的某段旅程,仅仅是有些人仅仅是某阶段出现,有些人却陪伴自己非常久.就像小学.中学.高中.大学,那些以前以为会长久拥有的,当经历过天涯各地地忙碌于生活,或如意.或失 ...

  5. Android-管理Activity生命周期 -暂停和恢复一个Activity

    在正常的使用app时,前台的activity有时候会被可见的组件阻塞导致activity暂停.比如,当打开一个半透明的activity(就像打开了一个对话框),之前的activity就会暂停.只要ac ...

  6. 记一个复杂组件(Filter)的从设计到开发

    此文前端框架使用 rax,全篇代码暂未开源(待开源) 原文链接地址:Nealyang/PersonalBlog 前言 貌似在面试中,你如果设计一个 react/vue 组件,貌似已经是司空见惯的问题了 ...

  7. Uber选拔专车司机:五年以上驾驶经验 两小时视频培训

    摘要:说起当时下流行打车软件Uber的司机,还得从春节前在上海一次打车说起.那几天,记者在上海某商场逛到打烊时间,大包小包拎着袋子根本腾不出手拦出租车,而商场门口的出租车临时停靠点更是挤满“血拼”而归 ...

  8. 记一个关于std::unordered_map并发访问的BUG

    前言 刷题刷得头疼,水篇blog.这个BUG是我大约一个月前,在做15445实现lock_manager的时候遇到的一个很恶劣但很愚蠢的BUG,排查 + 摸鱼大概花了我三天的时间,根本原因是我在使用s ...

  9. React中Ref 的使用 React-踩坑记_05

    React中Ref 的使用 React v16.6.3 在典型的React数据流中,props是父组件与其子组件交互的唯一方式.要修改子项,请使用new props 重新呈现它.但是,在某些情况下,需 ...

  10. 记一个社交APP的开发过程——基础架构选型(转自一位大哥)

    记一个社交APP的开发过程——基础架构选型 目录[-] 基本产品形态 技术选型 最近两周在忙于开发一个社交App,因为之前做过一点儿社交方面的东西,就被拉去做API后端了,一个人头一次完整的去搭这么一 ...

随机推荐

  1. channel 是怎么走上死锁这条路的

    本篇文章接着 hello world 的并发实现一文介绍 Go 的 channel 类型,同时进一步介绍 channel 的几种死锁情况,这些都是代码中很容易遇到的,要重点摘出来讲,防止一不留神程序就 ...

  2. Java 子父类型集合之间的转换

    假设现在有这样一个方法,入参是父类型的集合参数,这是个通用方法,你需要共用它,你现在要传子类型集合进去,怎么办? class Animal { } class Dog extends Animal { ...

  3. [粘贴]TiFlash

    TiFlash 是 TiDB HTAP 形态的关键组件,它是 TiKV 的列存扩展,在提供了良好的隔离性的同时,也兼顾了强一致性.列存副本通过 Raft Learner 协议异步复制,但是在读取的时候 ...

  4. [转帖]goproxy 使用说明

    Go 版本要求 建议您使用 Go 1.13 及以上版本, 可以在这里下载最新的 Go 稳定版本. 配置 Goproxy 环境变量 Bash (Linux or macOS) export GOPROX ...

  5. [转帖]Jmeter接口测试:参数化

    Jmeter接口请求中的参数经常需要通过参数进行赋值 引用形式:${} 变量时:${变量名} 函数时,${_函数名(参数1,参数2,参数3)} 值中"${n}"中,n为变量名:&q ...

  6. [转帖]chrome历史版本及重大变化(维基百科)

    Google Chrome是Google LLC开发的免费 网络浏览器.开发过程分为不同的"发布渠道",每个发布渠道都在单独的开发阶段进行构建.Chrome提供了4种渠道:稳定版, ...

  7. 【转帖】sqlserver 在高并发的select,update,insert的时候出现死锁的解决办法

    最近在使用过程中使用SqlServer的时候发现在高并发情况下,频繁更新和频繁查询引发死锁.通常我们知道如果两个事务同时对一个表进行插入或修改数据,会发生在请求对表的X锁时,已经被对方持有了.由于得不 ...

  8. jcmd的简单总结

    jcmd的简单总结 背景 自从2019年公司转向java技术路线. 一直断断续续的在学习java相关的技术内容. 但是总感觉学的不是很深入. 这周比较累.也不想在学新东西了. 所以想着再总结一下jcm ...

  9. 替换 &开头。;结尾之间的内容。用空格代替他们

    替换 &开头.;结尾之间的内容.用空格代替他们 var regExp = /\&.*?\;/g; var str = '123&asdsa;dqwe'; str = str.r ...

  10. [西湖论剑 2022]web部分题解(更新中ing

    [西湖论剑 2022]Node Magical Login 环境!启动!(ノへ ̄.) 这么一看好像弱口令啊,(不过西湖论剑题目怎么会这么简单,当时真的傻),那就bp抓包试一下(这里就不展示了,因为是展 ...