
壹 ❀ 引
因為現在工作主要以修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
以前沒出現但現在出現,更大概率是上層使用者不當所導致。
回到報錯的代碼,注意上圖中執行這段代碼的判斷條件,此時data
為null
,前面問題也說了,第一次點擊報錯,第二次點擊正常,那么正常情況下這里會怎么執行呢?於是我又斷點查看正常情況下的代碼執行,如下圖:

當第二次點擊改狀態,發現此時的data
是一個空數組,因為!data
為false
所以沒進入執行這段有問題的代碼,彈窗才能順利彈出,那么問題就來了,這個data
從何而來。於是我又閱讀了當前報錯的組件對於props
的定義,發現代碼中確實為當前組件定義了data
默認值,只是這個默認值是null
。

那么現在我們可以大膽猜測,第一點擊行為要么上層組件傳遞了data
為undefined
,要么沒傳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
等同於沒傳遞,那么組件有默認值肯定就會用默認值,這也解釋了為什么第一次點擊data
是null
,導致了程序報錯。

而上述代碼其實也在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
未能成功執行,是因為state
與ownProps
沒有改變,所以未能觸發此方法執行,但大致上我們還是證明了之前推測,組件永遠都是自己先渲染一遍,此后因為store
或者外部props
發生改變后再次觸發組件渲染。
回到問題本身,我們前面也說了,當關聯wiki沒有數據時,本身預期的就是一個空數組,而因為不合規的代碼書寫,導致了關聯wiki默認值成了undefined
,怎么修改呢?其實就一句的修改,將state
初始化改為:
state = {
wikiPages: []
};
刷新頁面,問題順利解決,總結來說,這是一個對於react
聲明周期不熟悉,以及不太友好的代碼書寫造成的bug
,排查兩小時,修改一個詞。另外再次強調,初始化state
不要通過this.props
這種方式賦值,因為你無法預估它的默認值是什么,以及下層組件會怎么使用,大概如此了。
叄 ❀ 總
老實說,修bug
的工作確實很像偵破一個案件,我需要搜集有限的信息,排查,還原現場,組合各種情況不斷去縮小范圍,最終確認問題點,並給出影響范圍最小的修改方案,這個過程有點枯燥,也有點有趣。之后應該也會不間斷更新一些奇怪bug
的排查思路,給自己排坑,避免自己也寫出類似的代碼,那么本文結束。