本文是『horseshoe·React專題』系列文章之一,後續會有更多專題推出
來我的 GitHub repo 閱讀完整的專題文章
來我的 個人部落格 獲得無與倫比的閱讀體驗
生命週期,顧名思義,就是從生到死的過程。
而生命週期鉤子,就是從生到死過程中的關鍵節點。
普通人的一生有哪些生命週期鉤子呢?
- 出生
- 考上大學
- 第一份工作
- 買房
- 結婚
- 生子
- 孩子的生命週期鉤子
- 退休
- 臨終遺言
每到關鍵節點,我們總希望有一些沉思時刻,因為這時候做出的決策會改變人生的走向。
React元件也一樣,它會給開發者一些沉思時刻,在這裡,開發者可以改變元件的走向。
非同步渲染下的生命週期
React花了兩年時間祭出Fiber渲染機制。
簡單來說,React將diff的過程叫做Reconciliation。以前這一過程是一氣呵成的,Fiber機制把它改成了非同步。非同步技能將在接下來的版本中逐步解鎖。
明明是一段同步程式碼,怎麼就非同步了呢?
原理是Fiber把任務切成很小的片,每執行一片就把控制權交還給主執行緒,待主執行緒忙完手頭的活再來執行剩下的任務。當然如果某一片的執行時間就很長(比如死迴圈),那就沒主執行緒什麼事了,該崩潰崩潰。
這會給生命週期帶來什麼影響呢?
影響就是掛載和更新之前的生命週期都變的不可靠了。
為什麼這麼講?因為Reconciliation這個過程有可能暫停然後繼續執行,所以掛載和更新之前的生命週期鉤子就有可能不執行或者多次執行,它的表現是不可預期的。
因此16之後的React生命週期迎來了一波大換血,以下生命週期鉤子將被逐漸廢棄:
- componentWillMount
- componentWillReceiveProps
- componentWillUpdate
看出特點了麼,都是帶有will
的鉤子。
目前React為這幾個生命週期鉤子提供了別名,分別是:
- UNSAFE_componentWillMount
- UNSAFE_componentWillReceiveProps
- UNSAFE_componentWillUpdate
React17將只提供別名,徹底廢棄這三個大活寶。取這麼個別名意思就是讓你用著噁心。
constructor()
React借用class類的constructor
充當初始化鉤子。
React幾乎沒做什麼手腳,但是因為我們只允許通過特定的途徑給元件傳遞引數,所以constructor
的引數實際上是被React規定好的。
React規定constructor
有三個引數,分別是props
、context
和updater
。
props
是屬性,它是不可變的。context
是全域性上下文。updater
是包含一些更新方法的物件,this.setState
最終呼叫的是this.updater.enqueueSetState
方法,this.forceUpdate
最終呼叫的是this.updater.enqueueForceUpdate
方法,所以這些API更多是React內部使用,暴露出來是以備開發者不時之需。
在React中,因為所有class元件都要繼承自Component
類或者PureComponent
類,因此和原生class寫法一樣,要在constructor
裡首先呼叫super
方法,才能獲得this
。
constructor
生命週期鉤子的最佳實踐是在這裡初始化this.state
。
當然,你也可以使用屬性初始化器來代替,如下:
import React, { Component } from 'react';
class App extends Component {
state = {
name: 'biu',
};
}
export default App;
複製程式碼
componentWillMount()
?這是React不再推薦使用的API。
這是元件掛載到DOM之前的生命週期鉤子。
很多人會有一個誤區:這個鉤子是請求資料然後將資料插入元素一同掛載的最佳時機。
其實componentWillMount
和掛載是同步執行的,意味著執行完這個鉤子,立即掛載。而向伺服器請求資料是非同步執行的。所以無論請求怎麼快,都要排在同步任務之後再處理,這是輩分問題。
也就是說,永遠不可能在這裡將資料插入元素一同掛載。
並不是說不能在這裡請求資料,而是達不到你臆想的效果。
它被廢棄的原因主要有兩點:
- 本來它就沒什麼用。估計當初是為了成雙成對所以才創造了它吧。
- 如果它宣告瞭定時器或者訂閱器,在服務端渲染中,
componentWillUnmount
生命週期鉤子中的清除程式碼不會生效。因為如果元件沒有掛載成功,componentWillUnmount
是不會執行的。姚明說的:沒有掛載就沒有解除安裝。 - 在非同步渲染中,它的表現不穩定。
初始化this.state
應該在constructor
生命週期鉤子中完成,請求資料應該在componentDidMount
生命週期鉤子中完成,所以它不僅被廢棄了,連繼任者都沒有。
static getDerivedStateFromProps(props, state)
?這是React v16.3.0釋出的API。
首先,這是一個靜態方法生命週期鉤子。
也就是說,定義的時候得在方法前加一個static
關鍵字,或者直接掛載到class類上。
簡要區分一下例項方法和靜態方法:
- 例項方法,掛載在
this
上或者掛載在prototype
上,class類不能直接訪問該方法,使用new
關鍵字例項化之後,例項可以訪問該方法。 - 靜態方法,直接掛載在class類上,或者使用新的關鍵字
static
,例項無法直接訪問該方法。
問題是,為什麼getDerivedStateFromProps
生命週期鉤子要設計成靜態方法呢?
這樣開發者就訪問不到this
也就是例項了,也就不能在裡面呼叫例項方法或者setsState了。
import React, { Component } from 'react';
class App extends Component {
render() {
return (
<div>React</div>
);
}
static getDerivedStateFromProps(props, state) {}
}
export default App;
複製程式碼
這個生命週期鉤子的使命是根據父元件傳來的props按需更新自己的state,這種state叫做衍生state。返回的物件就是要增量更新的state。
它被設計成靜態方法的目的是保持該方法的純粹,它就是用來定義衍生state的,除此之外不應該在裡面執行任何操作。
這個生命週期鉤子也經歷了一些波折,原本它是被設計成初始化
、父元件更新
和接收到props
才會觸發,現在只要渲染就會觸發,也就是初始化
和更新階段
都會觸發。
render()
作為一個元件,最核心的功能就是把元素掛載到DOM上,所以render
生命週期鉤子是一定會用到的。
render
生命週期鉤子怎麼接收模板呢?當然是你return給它。
但是不推薦在return之前寫過多的邏輯,如果邏輯過多,可以封裝成一個函式。
render() {
// 這裡可以寫一些邏輯
return (
<div>
<input type="text" />
<button>click</button>
</div>
);
}
複製程式碼
注意,千萬不要在render
生命週期鉤子裡呼叫this.setState
,因為this.setState
會引發render,這下就沒完沒了了。主公,有內奸。
componentDidMount()
這是元件掛載到DOM之後的生命週期鉤子。
這可能是除了render
之外最重要的生命週期鉤子,因為這時候元件的各方面都準備就緒,天地任你闖。
這就是社會哥,人狠話不多。
componentWillReceiveProps(nextProps)
?這是React不再推薦使用的API。
componentWillReceiveProps
生命週期鉤子只有一個引數,更新後的props。
該宣告周期函式可能在兩種情況下被觸發:
- 元件接收到了新的屬性。
- 元件沒有收到新的屬性,但是由於父元件重新渲染導致當前元件也被重新渲染。
初始化時並不會觸發該生命週期鉤子。
同樣,因為Fiber機制的引入,這個生命週期鉤子有可能會多次觸發。
shouldComponentUpdate(nextProps, nextState)
這個生命週期鉤子是一個開關,判斷是否需要更新,主要用來優化效能。
有一個例外,如果開發者呼叫this.forceUpdate
強制更新,React元件會無視這個鉤子。
shouldComponentUpdate
生命週期鉤子預設返回true。也就是說,預設情況下,只要元件觸發了更新,元件就一定會更新。React把判斷的控制權給了開發者。
不過周到的React還提供了一個PureComponent
基類,它與Component
基類的區別是PureComponent
自動實現了一個shouldComponentUpdate
生命週期鉤子。
對於元件來說,只有狀態發生改變,才需要重新渲染。所以shouldComponentUpdate
生命週期鉤子暴露了兩個引數,開發者可以通過比較this.props
和nextProps
、this.state
和nextState
來判斷狀態到底有沒有發生改變,再相應的返回true或false。
什麼情況下狀態沒改變,卻依然觸發了更新呢?舉個例子:
父元件給子元件傳了一個值,當父元件狀態變化,即便子元件接收到的值沒有變化,子元件也會被迫更新。這顯然是非常不合理的,React對此無能為力,只能看開發者的個人造化了。
import React, { Component } from 'react';
import Child from './Child';
class App extends Component {
state = { name: 'React', star: 1 };
render() {
const { name, star } = this.state;
return (
<div>
<Child name={name} />
<div>{star}</div>
<button onClick={this.handle}>click</button>
</div>
);
}
handle = () => {
this.setState(prevState => ({ star: ++prevState.star }));
}
}
export default App;
複製程式碼
import React, { Component } from 'react';
class Child extends Component {
render() {
return <h1>{this.props.name}</h1>;
}
shouldComponentUpdate(nextProps, nextState) {
if (this.props === nextProps) {
return false;
} else {
return true;
}
}
}
export default Child;
複製程式碼
同時要注意引用型別的坑。
下面這種情況,this.props
和nextProps
永遠不可能相等。
import React, { Component } from 'react';
import Child from './Child';
class App extends Component {
state = { name: 'React', star: 1 };
render() {
return (
<div>
<Child name={{ friend: 'Vue' }} />
<div>{this.state.star}</div>
<button onClick={this.handle}>click</button>
</div>
);
}
handle = () => {
this.setState(prevState => ({ star: ++prevState.star }));
}
}
export default App;
複製程式碼
import React, { Component } from 'react';
class Child extends Component {
render() {
return <h1>{this.props.friend}</h1>;
}
shouldComponentUpdate(nextProps, nextState) {
if (this.props === nextProps) {
return false;
} else {
return true;
}
}
}
export default Child;
複製程式碼
解決方法有兩個:
- 比較
this.props.xxx
和nextProps.xxx
。 - 在父元件用一個變數將引用型別快取起來。
所以this.state
和nextState
是隻能用第一種方法比較了,因為React每次更新state都會返回一個新物件,而不是修改原物件。
componentWillUpdate(nextProps, nextState)
?這是React不再推薦使用的API。
shouldComponentUpdate
生命週期鉤子返回true,或者呼叫this.forceUpdate
之後,會立即執行該生命週期鉤子。
要特別注意,componentWillUpdate
生命週期鉤子每次更新前都會執行,所以在這裡呼叫this.setState
非常危險,有可能會沒完沒了。
同樣,因為Fiber機制的引入,這個生命週期鉤子有可能會多次呼叫。
getSnapshotBeforeUpdate(prevProps, prevState)
?這是React v16.3.0釋出的API。
顧名思義,儲存狀態快照用的。
它會在元件即將掛載時呼叫,注意,是即將掛載。它甚至呼叫的比render
還晚,由此可見render
並沒有完成掛載操作,而是進行構建抽象UI的工作。getSnapshotBeforeUpdate
執行完就會立即呼叫componentDidUpdate
生命週期鉤子。
它是做什麼用的呢?有一些狀態,比如網頁滾動位置,我不需要它持久化,只需要在元件更新以後能夠恢復原來的位置即可。
getSnapshotBeforeUpdate
生命週期鉤子返回的值會被componentDidUpdate
的第三個引數接收,我們可以利用這個通道儲存一些不需要持久化的狀態,用完即可捨棄。
很顯然,它是用來取代componentWillUpdate
生命週期鉤子的。
意思就是說呀,開發者一般用不到它。
componentDidUpdate(nextProps, nextState, snapshot)
這是元件更新之後觸發的生命週期鉤子。
搭配getSnapshotBeforeUpdate
生命週期鉤子使用的時候,第三個引數是getSnapshotBeforeUpdate
的返回值。
同樣的,componentDidUpdate
生命週期鉤子每次更新後都會執行,所以在這裡呼叫this.setState
也非常危險,有可能會沒完沒了。
componentWillUnmount()
這是元件解除安裝之前的生命週期鉤子。
為什麼元件快要解除安裝了還需要沉思時刻呢?
因為開發者要擦屁股吖。
React的最佳實踐是,元件中用到的事件監聽器、訂閱器、定時器都要在這裡銷燬。
當然我說的事件監聽器指的是這種:
componentDidMount() {
document.addEventListener('click', () => {});
}
複製程式碼
因為下面這種React會自動銷燬,不勞煩開發者了。
render(
return (
<button onClick={this.handle}>click</button>
);
)
複製程式碼
componentDidCatch(error, info)
?這是React v16.3.0釋出的API。
它主要用來捕獲錯誤並進行相應處理,所以它的用法也比較特殊。
定製一個只有componentDidCatch
生命週期鉤子的ErrorBoundary
元件,它只做一件事:如果捕獲到錯誤,則顯示錯誤提示,如果沒有捕獲到錯誤,則顯示子元件。
將需要捕獲錯誤的元件作為ErrorBoundary
的子元件渲染,一旦子元件丟擲錯誤,整個應用依然不會崩潰,而是被ErrorBoundary
捕獲。
import React, { Component } from 'react';
class ErrorBoundary extends Component {
state = { hasError: false };
render() {
if (this.state.hasError) {
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
componentDidCatch(error, info) {
this.setState({ hasError: true });
}
}
export default ErrorBoundary;
複製程式碼
import React from 'react';
import ErrorBoundary from './ErrorBoundary';
import MyWidget from './MyWidget';
const App = () => {
return (
<ErrorBoundary>
<MyWidget />
</ErrorBoundary>
);
}
export default App;
複製程式碼
生命週期
這麼多生命週期鉤子,實際上總結起來只有三個過程:
- 掛載
- 更新
- 解除安裝
掛載和解除安裝只會執行一次,更新會執行多次。
一個完整的React元件生命週期會依次呼叫如下鉤子:
old lifecycle
-
掛載
- constructor
- componentWillMount
- render
- componentDidMount
-
更新
- componentWillReceiveProps
- shouldComponentUpdate
- componentWillUpdate
- render
- componentDidUpdate
-
解除安裝
- componentWillUnmount
new lifecycle
-
掛載
- constructor
- getDerivedStateFromProps
- render
- componentDidMount
-
更新
- getDerivedStateFromProps
- shouldComponentUpdate
- render
- getSnapshotBeforeUpdate
- componentDidUpdate
-
解除安裝
- componentWillUnmount
元件樹生命週期呼叫棧
應用初次掛載時,我們以render
和componentDidMount
為例,React首先會呼叫根元件的render
鉤子,如果有子元件的話,依次呼叫子元件的render
鉤子,呼叫過程其實就是遞迴的順序。
等所有元件的render
鉤子都遞迴執行完畢,這時候執行權在最後一個子元件手裡,於是開始觸發下一輪生命週期鉤子,呼叫最後一個子元件的componentDidMount
鉤子,然後呼叫棧依次往上遞迴。
元件樹的生命週期呼叫棧走的是一個Z字形。
如果根元件沒有定義A生命週期鉤子而子元件定義了,那呼叫棧就從這個子元件的A生命週期鉤子開始。
另外,只要元件內定義了某個生命週期鉤子,即便它沒有任何動作,也會執行。
app.render();
child.render();
grandson.render();
// divide
grandson.componentDidMount();
child.componentDidMount();
app.componentDidMount();
// divide
app.render();
child.render();
grandson.render();
// divide
grandson.componentDidUpdate();
child.componentDidUpdate();
app.componentDidUpdate();
複製程式碼
當然,componentWillMount、componentWillReceiveProps和componentWillUpdate生命週期鉤子有可能被打斷執行,也有可能被多次呼叫,表現是不穩定的。所以React決定逐步廢棄它們。
不過了解整個應用生命週期的正常呼叫順序,還是有助於理解React的。
React專題一覽