文章標題總算是可以正常一點了……
通過之前的文章我們已經知道:在 React 體系中所謂的 "在 JavaScript 中編寫 HTML 程式碼" 指的是 React 擴充套件了 JavaScript 的語法,也就是 JSX。JSX 語法中可以以類似 HTML 語法的方式使用 React 元件,從而編寫 React 元件就有一種創造一個新的 HTML 標籤的體驗。
上一篇文章《玩轉 React(四)- 創造一個新的 HTML 標籤》介紹瞭如何來建立一個 React 元件,以及元件的屬性。瞭解到元件的檢視是屬性的對映,通過改變元件屬性可以觸發元件重新渲染,從而改變元件的檢視。其實元件的檢視並不僅僅是由屬性對映來的,本篇將介紹另一種可以觸發元件重新渲染的方式,即元件的內部狀態(state),嚴格來說元件的檢視是由屬性和內部狀態對映而來的,即:view = f(props, state)
,跟屬性類似,狀態的改變也會觸發元件重新渲染,只不過狀態是元件內部基於自身邏輯或者使用者事件自己維護的,而不是由外部輸入的。
另外本文中會介紹一個通過類繼承方式定義的元件的生命週期,以及在各個生命週期函式中能做什麼,不能或儘量不要做什麼。
內容摘要
ReactDOM.render
在一個單頁面 web 應用中通常只呼叫一次。- 元件可以通過
setState
改變內部狀態state
來更新檢視。 setState
多數情況下是非同步的。- 不要直接使用當前
state
的值生成下一個state
。 - 不要直接通過
this.state
修改state
。 - 元件生命週期流程圖。
- 各個生命週期函式介紹及使用經驗。
以上是本文的內容摘要,如果你已經知道我要說的是什麼,那麼就沒有必要繼續看下去了,節約時間。
元件的內部狀態
此前,我們已經瞭解到可以通過 ReactDOM.render(<HelloMessage name="Lucy" />, container)
的方式,將帶有特定屬性的元件渲染到頁面的某個 DOM 節點中(container),這樣頁面上會展示出 “Hello Lucy”,當我們希望頁面上展示 “Hello Tom” 的時候,我們可以將元件的 name 屬性改為 Tom 後再次呼叫 ReactDOM.render
方法,這樣元件就會以新的屬性重新渲染,從而更新元件的檢視。
下面是官方文件中一個展示時鐘的例子,我簡單改造了下:
例子中定義了一個 Clock
元件,元件接收一個 time
屬性,在元件外部通過 setInterval
週期性地呼叫 ReactDOM.render
不斷更新 Clock
的屬性並重新渲染。
然而在很多實際場景中,對於一個時鐘元件,我們希望它有更好的封裝性和複用性,也就是說我們希望只呼叫一次 ReactDOM.render(<Clock />, container)
然後它可以自己更新自己的檢視,這樣我們就更容易在頁面上放置多個時鐘了,即複用性更好了。
要達到這個目的,就需要元件的內部狀態來支援。元件有一個特殊的屬性 state
用來儲存元件的內部狀態。使用者可以通過 this.setState(statePatch)
來更新元件的狀態,元件的狀態更新後會重新執行 render
方法來更新檢視,上面的例子使用內部狀態改造後:
這樣 Clock
作為一個完整的時鐘元件就可以自己來更新自己了,上篇文中也有提到過,如果想要使用元件的內部狀態,那元件必須以類繼承的方式來定義,而不能使用函式式元件。所以說,函式式元件經常也被稱作是無狀態元件(stateless)。
上面例子中有用到 componentDidMount
和 componentWillUnmount
兩個函式,它們是元件的生命週期函式,本文的後半部分將會介紹,這倆函式分別在元件掛載到頁面上和元件將要從頁面上移除時呼叫。
改造後的例子,我們只需要呼叫一次 ReactDOM.render
即可,在實際的專案中,一個完整的單頁面 web 應用,也只需要呼叫一次 ReactDOM.render
方法把根元件掛載到頁面中即可,剩下的工作就都放心地交給 React 就行了。
初始化元件內部狀態
在建立一個擁有內部狀態的元件時,我們需要對內部狀態進行初始化,即設定元件最初的狀態是什麼。做法很簡單,就是在建構函式 constructor
中設定 state
屬性就可以了。如下所示:
class MyComponent extends React.Component {
constructor(props) {
super(props); // 這行程式碼不能少哦
this.state = {
name: "Lucy"
}
}
}複製程式碼
setState 大多數情況下是非同步的
setState
多數情況下是非同步的,非同步意味著通過 setState
更新元件狀態後,不能立刻通過 this.state
來獲取到更新之後的值,另外當連續多次呼叫 setState
來更新同一個欄位時,只有最後一次更新才會生效。如下示例:
如果希望上面示例程式碼正常工作,你需要通過回撥函式的方式來生成下一個 state,如下所示:
this.setState(preState => ({value: preState.value + 1}));
this.setState(preState => ({value: preState.value + 2}));
this.setState(preState => ({value: preState.value + 3}));複製程式碼
所以,直接基於當前 state
的值,生成一下個 state
是不靠譜的,但是很多不清楚這一點的同學基本上都是這麼做的,因為寫起來簡單嘛,而且貌似也沒有什麼問題。這是因為很多情況下,業務邏輯沒有那麼複雜,基本不會頻繁呼叫 setState
。但是這確實是一個隱患,如果在專案初期不注意規避,等專案複雜到一定程度以後,可能會出現難以排查的BUG。
那為什麼說多數情況下是非同步的呢?難道有些情況下不是非同步的嗎?是的,實際上只有在 React 能控制的事件處理過程中呼叫的 setState
才是非同步的,如:生命週期函式,React 內建的如 button,input 等元件的事件處理函式。在多數的情況下我們只需要在這些地方控制我們的元件就夠了,所以說大多數情況下 setState
是非同步的。
在某些特殊的元件中,可能需要通過 addEventListener
來設定某些 DOM 的事件處理函式,在這種通過原生的 JS API 來設定的事件處理過程呼叫 setState
就是同步的,會立即更新 this.state
。另外還有 setInterval
、setTimeout
等原生 API 的回撥函式也是如此。
不要直接通過 this.state 來更新元件狀態
這一點跟屬性類似,直接通過 this.state
修改元件狀態,元件狀態被修改了,但並不會觸發元件的重新渲染。這樣就會導致元件檢視與狀態不一致。
生命週期函式
一個元件被我們創造到這個世界上之後,在使用它時,它的每個例項都是有一定生命週期的,下面這張圖說明了一個元件例項的生命週期:
圖片來源:tylermcginnis.com/an-introduc…
這張圖略微有點老,不過結合下文來看也沒什麼問題,下面我們來解釋一下上面這張圖。
元件初始化:constructor
我們定義的每一個元件,都是一個類(class),這些類被例項化後才能作為 React DOM 中的一個節點渲染到頁面上。所以,當我們通過 ReactDOM.render
或者在某個元件中通過 JSX 表示式將一個元件第一次渲染到頁面上時,元件首先要做的就是對元件進行例項化。
例項化主要做的事情:
- 建立一個元件的例項物件(也就是 Element,通常對應一個JSX表示式,如:
<MyComponent />
)。 - 獲取元件的預設屬性。
- 獲取元件的初始內部狀態(在
constructor
中this.state = xxxx;
)。
componentWillMount
在元件被渲染到頁面上之前執行,在元件的整個生命週期內只執行一次。在這裡可以呼叫 setState
更新內部狀態,但是更推薦將這裡的狀態更新操作放到 constructor
中。
該函式執行完後會立馬執行 render
方法並將元件渲染到頁面上。所以,在這裡執行 setState 不會觸發額外的渲染過程,因為這是沒有必要的。
componentDidMount
元件被渲染到頁面上後立馬執行,在元件的整個生命週期內只執行一次。這個時候是做如下操作的好時機:
- 某些依賴元件 DOM 節點的操作。
- 發起網路請求。
- 設定
setInterval
、setTimeout
等計時器操作。
在這裡可以呼叫 setState
更新元件內部狀態,且會觸發一個重新渲染的過程,即會重新執行 render
方法並更新檢視。
componentWillReceiveProps
componentWillReceiveProps(nextProps)複製程式碼
該宣告周期函式可能在兩種情況下被呼叫:
- 元件接收到了新的屬性。新的屬性會通過
nextProps
獲取到。 - 元件沒有收到新的屬性,但是由於父元件重新渲染導致當前元件也被重新渲染。
你只要知道,當該函式被呼叫時,並不一定是因為屬性發生了變化。
在這裡也可以呼叫 setState
更新元件的內部狀態,同樣也不會觸發額外的重新渲染操作,React 會聰明地用更新後的屬性和內部狀態進行一次重新渲染。
shouldComponentUpdate
shouldComponentUpdate(nextProps, nextState)複製程式碼
這是一個詢問式的生命週期函式,所以該函式需要一個返回值 true/false
,如果為 true
,元件將觸發重新渲染過程,如果為 false
元件將不會觸發重新渲染。因此,合理地利用該函式可以一定程度節省開銷,提高系統的效能。
此處不能呼叫 setState
更新元件的狀態。
由於元件屬性或者內部狀態被改變時都觸發元件重新渲染,所以該函式接受兩個引數:新的屬性(nextProps)、新的狀態(nextState)。
在處理該宣告周期函式時,切記要兼顧屬性和狀態,不能只顧其一,不然很容易踩坑。例如:某位同學只依據屬性來判斷是否觸發重新渲染,而忽略了內部狀態,這樣就導致你無論如何 setState
,元件檢視都不能正常更新。
在上篇文章中我們提到類繼承方式定義元件時說到,React 提供了兩個基類,一個是 Component
,另一個是 PureComponent
,兩者的差別就在於後者已經幫我們簡單實現了一下 shouldComponentUpdate
函式,當屬性和狀態都沒有發生變化時返回 false
以避免額外的開銷。
但是比對過程出於效能考慮,只是進行淺比對,也就是隻比對物件的第一級欄位,而且是否發生變化是通過 Object.is 方法類判斷的。所以會導致有時候發生變化了元件沒有更新,沒有變化卻觸發了重新渲染過程。這個在這裡不再贅述,想深入探討可以掃描問候的二維碼加我微信好友(我的微信:leobaba88)。
componentWillUpdate
當元件 shouldComponentUpdate
返回 true
或者呼叫 forceUpdate
時將觸發此函式。
該函式中不能呼叫 setState
更新元件狀態,當你想這麼做的時候,你可以考慮將它移到 componentWillReceiveProps
函式裡。
該函式在函式第一次渲染的時候不會執行。
componentDidUpdate
componentDidUpdate(prevProps, prevState)複製程式碼
在元件重新渲染過程中,重新執行 render
方法並更新元件檢視後立即執行該函式。類似元件第一次渲染過程中的 componentDidMount
,該函式在第一次渲染時不會執行。
在此處是做這些事情的好時機:
- 執行依賴新 DOM 節點的操作。
- 依據新的屬性發起新的網路請求。(但是此處一定要格外謹慎,一定要在確認屬性變化後再發起網路請求,不然極有可能進入死迴圈:didUpdate -> ajax -> changeProps -> didUpdate -> ...)。
componentWillUnmount
當元件被從頁面中移除之前呼叫,此時是清理戰場的好時機,如清理定時器、中指網路請求等。
componentDidCatch
componentDidCatch(error, info)複製程式碼
這是 React 16 新加入的一個生命週期函式。定義該生命週期函式的元件將會成為一個錯誤邊界,錯誤邊界這個詞非常形象,它可以有效地將錯誤限制在一個有限的範圍內,而不會導致整個應用崩潰,防止一顆耗子屎壞了一鍋湯。
錯誤邊界元件,可以捕獲其整個子元件樹內發生的任何異常,但是卻不能捕獲自身的異常。
下面是官方的一個示例,大家感受下:
最後(微信群)
這篇文章來的有點慢,非常抱歉。
另外為了方便大家閱讀,我將所有文章的連結更新到第一篇文章 《玩轉React(一)- 前言》 中。
文字的表現範圍畢竟有限,為了方便大家交流,我建了一個微信群,對 React 感興趣的同學可以進群一起交流、學習,由於微信群邀請的時間限制,大家可以先掃描下面二維碼,加我好友,我拉大家進群:
我的微信:leobaba88