【React】排查兩小時,修改一個詞,記一個因代碼書寫不規范導致的生命周期BUG


壹 ❀ 引

因為現在工作主要以修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的排查思路,給自己排坑,避免自己也寫出類似的代碼,那么本文結束。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM