本篇博文基於 React 16.5.2
吐槽:
作為一個後端開發,15年開始關注大前端發展趨勢,於17年去線下聽了場前端開發會議,那個時候Vue2.0剛出沒多久,就被那快速構建頁面給吸引了。最早重返前端還是大半年前,新專案用vue寫了幾個功能頁面,發現現在寫前端是真挺舒服,尤其是對於後端人員來說(排除掉CSS),快速入門並上手不是什麼問題。
至於為什麼最終選擇了react而非vue?是因為當時對react和vue及RN和weex做了番調研對比,介於weex的不給力,及後期react和vue學習成本差不多,但react的社群更為活躍,外加發起者背景,就毅然選擇了react。(我個人是通一精百的支持者,所以對於react的理念(learn once,write anywhere),是很贊成的。而weex的理念(write once,run anywhere)雖然很吸引人,但時下個人覺得技術並未達到此程度,配置的複雜度及大量的輪子需造,難以滿足大型專案的要求。包括目前JD推出的Taro,個人目前持觀望態度,等到react這塊應用到專案之後,再碼一波Taro實際調研一番)。
接觸React的時候已經是React 16.3,不禁感慨前端發展至今,越有後端的趨勢。前後花了3個多月的時間過了一遍webpack4,npm,react,在公司內部做了幾場培訓,發現了其中的一些不協調,但隨著版本的迭代,這些不協調也依次在被更正。(看React17的更新內容,原本一些摸稜兩可的方法、屬性元素命名均會得到改善:)
後續會以此文為契機,開一個專欄,記錄、分享公司現有電商專案逐步遷移至react技術棧。
正文
本篇博文會對React16.5.2中的常用生命週期函式做一些翻譯、講解說明及發表一些個人的理解和看法,如有錯誤、歧義之處,還望大家不吝嗇指出,互相交流:)
開篇借用官方提供的生命週期函式圖:(雖然是React 16.4的,但同樣適用於16.5版本)
出處:projects.wojtekmaj.pl/react-lifec…react元件生命週期函式說明官方地址:reactjs.org/docs/react-…
getDerivedStateFromProps()方法的16.3版本與之後的16.4和16.5有所調整:
在16.3版本上,該方法僅在props變動時才會被觸發。
在16.3之後的版本上,該方法調整為props和state的變動都會觸發該方法。
從上圖中,我們可以看到,React的生命週期函式主要集中在三個階段:掛載階段(Mounting),更新階段(Updating)和解除安裝階段(Unmounting)。
其中:
- Mounting階段主要有這幾個方法需要注意:
- constructor(props)
- componentWillReceiveProps(nextProps)
- getDerivedStateFromProps(props, state)
- componentWillMount()
- render()
- componentDidMount()
- Updating階段主要有這幾個方法需要注意:
- componentWillReceiveProps(nextProps)
- getDerivedStateFromProps(props, state)
- shouldComponentUpdate()
- componentWillUpdate()
- render()
- componentDidUpdate()
- Unmounting階段只有一個方法需要注意:
- componentWillUnmount()
- 額外的幾個方法說明:
- componentDidCatch()
- this.setState()
- this.forceUpdate()
下面我們就對以上生命週期函式做進一步說明:
Mounting:
constructor(props)
- 在建構函式中,一般就做兩件事情:
- 初始化state
- 在建構函式中初始化state需直接賦值,不能呼叫 this.setState() 方法。
- constructor()是唯一能夠讓你直接給 this.state 賦值的地方,可能有同學會說可以在建構函式外(跟constructor同級)直接給 state 賦值,確實,因為 state 派生自 Component 或者 PureComponent 模組,在外層直接賦值,其實是在給基類中的 state 賦值,而在 constructor() 使用 this.state = {...} 進行初始化,實際指的是當前元件這個例項。以下兩種賦值方法均可:
class TestContainer extends PureComponent { state = { content:"1" //先執行 } constructor(props){ super(props); this.state={ content:"2" //後執行,最終 this.state.content 為 2 } } 。。。 } 複製程式碼
- 繫結方法:this.handleClick = this.handleClick.bind(this);
- 我所認為的是:在建構函式中進行方法的繫結後,在使用的時候可以減少匿名方法的產生,進而提高效能。
- 初始化state
- 如果不需要初始化state,或者繫結方法,那麼可以不用實現constructor(props)。
- 如果實現了這個方法,那麼應當在方法體的最開始呼叫 super(props),否則會導致 this.props 為 undefined 的問題。
- 不應在該方法中進行"事件的訂閱/引入有side-effects的方法"。(這種應該放在 componentDidMount() 中)
這裡的 side-effects 方法,翻譯出來是 "副作用" 的意思,個人覺得不妥,有點生澀,可能翻譯成"附加作用"更為妥當。
它指的是,render()方法應該只完成它的主要功能(如初始化state和繫結方法),不應順帶完成其他的附加功能,如統計,動畫等。
componentWillReceiveProps(nextProps)
該方法的應用場景:根據特定的props變換,來觸發state的更新。如我們可能會有一個canvas用來表示頁面loading百分比,這個時候就可以根據傳入的nextProps中的百分比屬性跟當前state的百分比屬性做對比,若不一致,則更新,如下:
componentWillReceiveProps(nextProps) {
if (this.props.percent !== nextProps.percent) {
this.setUpCircle(nextProps.percent);
}
}
複製程式碼
- 該方法在React16版本中已經被標記為unsafe,雖然目前依然可以使用(即使用 componentWillReceiveProps 或者 UNSAFE_componentWillReceiveProps 均可),但在 React17 中將會移除,不再建議使用【該方法將會被 getDerivedStateFromProps() 取代。】。
- 在元件對新(下一個) props 執行任何操作之前,該方法會被觸發,並將 下一個props 作為引數。
- 初始渲染時該方法不會被觸發。
- 可以在該方法內呼叫 this.setState(),但並不建議呼叫,因為這會導致只要元件更新,這個方法就會被執行。
getDerivedStateFromProps(props, state)
該方法的應用場景:使元件能夠根據父元件傳入的 props 來更新其自身的 state
在16.4版本之前,只有props更新才會觸發該方法;但FB在16.4版本做了完善,目前 props 和 state 的更新均會觸發該方法。
- 該方法的兩個入參,分別表示:
- props:父元件傳入的值。可能重新命名為 nextProps 更為直觀。
- state:元件自身的state,相當於 this.state,可能重新命名為 prevState 更好理解。
- 該方法在呼叫 render() 方法前被觸發。
- 如果父元件的state進行了更新,那麼其子元件也會觸發 getDerivedStateFromProps() 方法。
- 使用該方法的時候需要初始化 state,否則在控制檯中會出現警告資訊,如下圖:
- 使用該方法,需要在該方法中返回一個:物件/null:
- 如果返回的是物件,則會更新 state。
- 如果返回的是null,則表示不更新。
- 該方法用於取代:componentWillReceiveProps(nextProps) 生命週期函式。
- getDerivedStateFromProps(props, state) 和 componentWillReceiveProps(nextProps) 的差異:
- componentWillReceiveProps(props, state):僅在父元件更新時觸發。
- getDerivedStateFromProps(props, state):除了父元件更新,其自身的state更新時也會觸發。
- getDerivedStateFromProps(props, state) 和 componentWillReceiveProps(nextProps) 的差異:
- 該方法無法訪問元件的例項,換句話說,不能在該方法內部,呼叫 this.state,this.myFunction() 等例項物件/方法。
componentWillMount()
使用場景:可以在根元件的componentWillMount中做一些App的配置。
- 該方法在React16版本中已經被標記為unsafe,雖然目前依然可以使用(即使用 componentWillMount 或者 UNSAFE_componentWillMount 均可),但在 React17 中將會移除,不再建議使用。
- 該方法將會被 getDerivedStateFromProps() 取代。
- 該方法是SSR(server-side render)渲染上唯一的生命週期函式。
- 不要在該方法中使用 rpc 請求載入元件的資料。
- 不能在該方法中呼叫 this.setState()。
render()
-
元件中唯一必須的方法。
-
render()方法的返回型別目前有 string,number,boolean,null,portal 以及 fragments(帶有key屬性的陣列),eg:
render() { return "string"; //string return 123; //number return true; //如果返回的是false,則什麼都不渲染 return null; //如果返回的是null,則什麼都不渲染 return ReactDOM.createPortal(<MyComponent></MyComponent>, domNode);//portal return [ <div key="unique_1">如果返回的是陣列型別</div>, <span key="unique_2">需要在每個html元素上加上 key 值</span>, //fragments <p key="unique_3">否則控制檯會報錯</p> ] } 複製程式碼
-
需要注意的是,元件在更新的時候,也會觸發 render() 方法,但如果 shouldComponentUpdate() 返回的是false,則在"更新階段",render()方法不會被觸發。
-
該方法應為一個純函式,除了做渲染的動作外,不應順帶完成其他動作(如 setState 等),即應避免 side-effects。
-
應儘量僅在 render() 這個生命週期函式中中從 this.props 和 this.state 中讀取資料。
componentDidMount()
使用場景:整體來說就是做“獲取一些資料”,或者做必須要有 DOM 才能做的設定。
- 只會觸發一次:在元件被掛載到頁面之後被觸發,之後就不會再執行了(如頁面的更新等都不會再觸發,需注意)。
- 一般一些 rpc 請求會放到這個方法中進行呼叫。
- 一般事件的訂閱會寫在該方法中(但是不要忘記在元件unmount的時候進行取消訂閱。)
- 一些需要初始化DOM節點的操作可以放在這個宣告周期函式中進行。
- 可以在該方法中使用 this.setState()。但需要注意,該操作會在瀏覽器更新螢幕之前發生,對於使用者而言是無感的(即在該方法中又額外呼叫了一次this.setState,導致render()觸發了2次,但使用者的感受依然是螢幕更新了一次)。不過要謹慎在該方法中呼叫 this.setState,會導致效能問題。
Mounting 階段,生命週期函式執行順序如下圖(包含子元件)
排除了 UNSAFE 的方法。
Updating:
componentWillReceiveProps(nextProps)
- 不再複述,具體 點選跳轉
getDerivedStateFromProps(props, state)
- 不再複述,具體 點選跳轉
shouldComponentUpdate(nextProps, nextState)
使用場景:一般用於精確控制元件是否需要重新渲染(這個方法一般用於效能優化),在絕大多數情況下,每次 state 的更改都應該讓元件重新渲染。
- 只要有一個欄位進行了更新,那麼其他所有欄位都會進行更新,這個會減慢頁面速度。(通過shouldComponentUpdate,允許我們只有當我們關心的 props 更新的時候,才進行元件的更新)。
- 但需要謹慎使用,因為一旦你自己忘記實現了這個方法,可能會導致你的react元件無法正常更新。
- 該方法返回的資料型別是一個boolean,用於決定元件是否應該更新。
- 預設返回 true。
- 當返回 false 的時候,阻止的是當前的元件進行更新,對於當前元件的"子元件",並不受影響。
- 不應該在繼承於 PureComponent 的元件中顯示實現 shouldComponentUpdate(),控制檯會有警告。
- 目前,當該方法返回false的時候,componentWillUpdate()、更新時的 render() 以及 componentDidUpdate() 都將不會被觸發。(FB在官網上說後期有可能會調整該方法:即使返回 false,依然有可能會觸發再次渲染,需留個心)
- 什麼時候該方法不會被觸發?
- 元件第一次渲染時。
- 在元件中呼叫 this.forceUpdate() 時。
- 個人建議:
- 該方法應儘量不去手動實現。
- 不應使用該方法來做一些防止元件渲染的操作,這可能會導致一些莫名的bug,如元件明明應該更新,但是卻沒有更新。
- 優先可以考慮繼承 PureComponent 而非 Component 來優化元件而不是手動實現shouldComponentUpdate。
- 不能在這個方法中使用 this.setState()。
componentWillUpdate(nextProps, nextState)
使用場景:當你實現了 shouldComponentUpdate (返回 true時) 並且在 props 更改時需要執行某些操作的時候,那麼 componentWillUpdate 這個方法還是有點意義的,但個人認為,作用不是太大,反倒是增加了理解的複雜度,被刪除也是情理之中。
- 該方法在React16版本中已經被標記為unsafe,雖然目前依然可以使用(即使用 componentWillUpdate 或者 UNSAFE_componentWillUpdate() 均可),但在 React17 中將會移除,不再建議使用。
- 從功能上來說,componentWillUpdate(nextProps, nextState) 跟 componentWillReceiveProps(nextProps) 基本相同,只是前者不允許呼叫 this.setState()。但是,這2個方法
- 當 shouldComponentUpdate 返回 false時, 該生命週期函式將不會被觸發。
- 不能使用setState。
render()
- 不再複述,具體 點選跳轉
componentDidUpdate(prevProps, prevState, snapshot)
使用場景:
- 一般在這個方法中進行 RPC 請求。(可以比較下先前的 props 和當前的 props 是否一致,若一致,則可以不用請求網路資料。)
- 如果想要在DOM自身更新之後做一些動作(如重新排列網格等),那麼可以在這個方法中進行。
- 該生命週期函式在元件更新完成後立即執行。元件第一次渲染的時候,該生命週期函式不會被觸發。
- 可以做跟 componentDidMount 中所作的相同的事情:如重置佈局,重繪畫布等
- 可以使用setState。
Updating 階段,生命週期函式執行順序如下圖(包含子元件)
排除了 UNSAFE 的方法。
Unmounting:
componentWillUnmount()
- 該方法在元件將要被解除安裝的時候觸發。
- 另外,在渲染期間,當前元件/其子元件的componentDidMount()函式發生錯誤時,該方法也將被觸發。
- eg:
- 我刻意在"巢狀在子元件中的元件"中的componentDidMount()方法中,丟擲了一個異常,此時該元件往上的父級元件均觸發了 componentWillUnmount()
-
- 我嘗試在construct(),componentDidUpdate()等生命週期函式中丟擲異常,均不會觸發 componentWillUnmount()。
Unmounting 階段,生命週期函式執行順序如下圖(包含子元件)
這裡需要注意下,該函式是 will unmount,所以觸發順序上是從父元件到子元件,但釋放順序上,是最內層的子元件先釋放,最終最外層的根元件才釋放。
額外的幾個方法說明:
componentDidCatch(error,info)
使用場景:UI中的一些JS錯誤不應該使整個App崩潰,為了解決這個問題,React16中引入了Error Boundary(錯誤邊界)這個概念,旨在解決允許頁面的部分元件異常但不影響App的渲染。可以認為是元件中的 try-catch。而為了實現這個功能,就需要藉助 componentDidCatch() 這個生命週期函式。
- Error Boundaris是自定義的 react 的元件,這些元件可以捕獲其子元件的異常,並顯示子元件錯誤時的替代元件內容,而App不會崩潰。
- 怎麼樣的元件可以認為是 Error Boundary 元件?
-
只要該元件內部實現了 componentDidCatch 這個方法就可以認為是 ErrorBoundary 元件,如:
... class MyErrorBoundary extends Component{ ... componentDidCatch(error,info){ ... } ... } 複製程式碼
-
- 一個例子:當子元件發生錯誤時,顯示"sth wrong here."
-
MyErrorBoundary:
import React, { Component } from 'react'; class MyErrorBoundary extends Component { state = { isError: false } render() { if (this.state.isError) { return (<div>sth wrong here.</div>); } return this.props.children; } componentDidCatch(error, info) { console.log(error, info); this.setState({ isError = true }); //也可以做其他的一些事情,如日誌,異常數統計等 } } export default MyErrorBoundary; 複製程式碼
-
App.js
... render(){ return ( ... <MyErrorBoundary> <OtherComponent /> </MyErrorBoundary> ... ); } ... 複製程式碼
-
- ErrorBoundary 元件只能捕獲其子元件,無法捕獲其自身的異常。
- 為什麼用 ErrorBoundary 這樣的元件?直接在程式碼中 try-catch 不是很直觀?
- 這個問題,就仁智各見了,try-catch 面向的是 程式碼,而 ErrorBoundary 面向的是元件:這裡我想用一個後端例子來說明,在寫後端程式碼時(比如一個 Web Api) ,你可以在具體業務程式碼中使用 try-catch 來對可能出現異常的程式碼塊做處理,但有的時候,你可能想針對所有“方法”做異常處理,這個時候使用 AOP 的方式來寫一個異常處理的方法更好(或者可以認為是 Exception filters這樣的自定義類)
- 注意事項:若要檢視ErrorBoundary的效果,無法在開發模式下進行(即mode=development)【或者直接執行 npm start,是不會出效果的,會顯示具體的錯誤資訊。】,有兩種方法可以解決:
-
- 使用 npm run build命令將專案打包後釋出到伺服器上進行;
-
- 將專案的模式從development調整為 production
- 需注意,若是使用create-react-app來建立的專案,配置檔案需要解包後才能進行編輯,執行命令: npm run eject,解包後調整"/scripts/build.js",將 process.env.NODE_ENV 賦值為 production,之後再執行 npm start 的時候就能看到上面示例程式碼的效果。
-
this.setState()
-
用來設定state的值。
-
在16.3開始,FB建議使用setState的非同步函式寫法,如: 原先我們在使用的時候是直接進行賦值:
inputHandler = (e) => { this.setState({ inputValue: e.target.value //不再這麼做 }); ) 複製程式碼
而是改用:
```javascript
inputHandler = (e) => {
let value = e.target.value;
this.setState(() => {
return {inputValue: value}; //使用方法的形式,最終再返回一個物件,這裡需要注意下,這麼寫是非同步的,但存在一個問題,即輸入的內容,需要再方法外層先獲取到:如這裡的value。
})
}
```
複製程式碼
-
非同步寫法中,該方法提供了一個回撥函式,通過該回撥函式,可以確保只有等到setState觸發完成之後,才會執行回撥的方法(另外一個可以確保在setState執行完成之後再執行的點是componentDidUpdate方法),如:
this.setState((prevState, nextState)=>{ ... }, callback); 複製程式碼
-
該方法提供了2個入參,prevState 和 props,前者相當於是 this.state。
-
只有當setState方法的第一個引數執行完成之後,才會執行 callback方法。
-
需要注意的是,因為setState是一個非同步方法,所以在賦值的時候需要注意下,如果需要從表單或者其他地方獲取值賦值給state的某一個屬性,需要先把這個值在setState方法之前賦給一個變數,再在setState方法中使用這個變數。如
let name = e.target.value; this.setState((prevState, props)=>{ return { userName : name //不能夠直接 userName:e.target.value,非同步方法中獲取不到當前上下文 } }, callback); 複製程式碼
-
-
補充:對於 setState ,其不一定在呼叫 setState 的時候就立即觸發這個動作。為了效能,react會自行判斷,將元件的所有setState在同一個時間點一同執行(批量執行),而非呼叫一次就執行一次。
-
每一次 setState 都會導致元件的再次渲染,除非 shouldComponentUpdate 返回 false。
this.forceUpdate()
- 呼叫forceUpdate()的時候,將會跳過 shouldComponentUpdate()而直接重新render()元件。
- 父元件中呼叫 forceUpdate 亦會導致子元件的生命週期函式被觸發(包括componentDidUpdate)。
- 正常情況下,這個方法很少使用。