React 生命周期很多人都了解,但通常我們所了解的都是**單個組件**的生命周期,但針對**Hooks 組件、多個關聯組件**(父子組件和兄弟組件) 的生命周期又是怎么樣的喃?你有思考和了解過嗎,接下來我們將完整的了解 React 生命周期。
關于**組件**,我們這里指的是`React.Component`以及`React.PureComponent`,但是否包括 Hooks 組件喃?
## 一、Hooks 組件
**函數組件**的本質是函數,沒有 state 的概念的,因此**不存在生命周期**一說,僅僅是一個**render 函數**而已。
但是引入**Hooks**之后就變得不同了,它能讓組件在不使用 class 的情況下使用 state 以及其他的 React特性,相比與 class 的生命周期概念來說,它更接近于實現狀態同步,而不是響應生命周期事件。但我們可以利用`useState`、`useEffect()`和`useLayoutEffect()`來模擬實現生命周期。
即:**Hooks 組件更接近于實現狀態同步,而不是響應生命周期事件**。
下面,是具體的 生命周期 與 Hooks 的**對應關系**:
* `constructor`:函數組件不需要構造函數,我們可以通過調用**`useState`來初始化 state**。如果計算的代價比較昂貴,也可以傳一個函數給`useState`。
~~~js
const [num, UpdateNum] = useState(0)
~~~
* `getDerivedStateFromProps`:一般情況下,我們不需要使用它,我們可以在**渲染過程中更新 state**,以達到實現`getDerivedStateFromProps`的目的。
~~~js
function ScrollView({row}) {
let [isScrollingDown, setIsScrollingDown] = useState(false);
let [prevRow, setPrevRow] = useState(null);
if (row !== prevRow) {
// Row 自上次渲染以來發生過改變。更新 isScrollingDown。
setIsScrollingDown(prevRow !== null && row > prevRow);
setPrevRow(row);
}
return `Scrolling down: ${isScrollingDown}`;
}
~~~
React 會立即退出第一次渲染并用更新后的 state 重新運行組件以避免耗費太多性能。
* `shouldComponentUpdate`:可以用?**`React.memo`**?包裹一個組件來對它的`props`進行淺比較
~~~js
const Button = React.memo((props) => {
// 具體的組件
});
~~~
注意:**`React.memo`?等效于?`PureComponent`**,它只淺比較 props。這里也可以使用`useMemo`優化每一個節點。
* `render`:這是函數組件體本身。
* `componentDidMount`,`componentDidUpdate`:?`useLayoutEffect`?與它們兩的調用階段是一樣的。但是,我們推薦你**一開始先用?useEffect**,只有當它出問題的時候再嘗試使用?`useLayoutEffect`。`useEffect`可以表達所有這些的組合。
~~~js
// componentDidMount
useEffect(()=>{
// 需要在 componentDidMount 執行的內容
}, [])
useEffect(() => {
// 在 componentDidMount,以及 count 更改時 componentDidUpdate 執行的內容
document.title = `You clicked ${count} times`;
return () => {
// 需要在 count 更改時 componentDidUpdate(先于 document.title = ... 執行,遵守先清理后更新)
// 以及 componentWillUnmount 執行的內容
} // 當函數中 Cleanup 函數會按照在代碼中定義的順序先后執行,與函數本身的特性無關
}, [count]); // 僅在 count 更改時更新
~~~
**請記得 React 會等待瀏覽器完成畫面渲染之后才會延遲調用?`useEffect`,因此會使得額外操作很方便**
* `componentWillUnmount`:相當于`useEffect`里面返回的`cleanup`函數
~~~js
// componentDidMount/componentWillUnmount
useEffect(()=>{
// 需要在 componentDidMount 執行的內容
return function cleanup() {
// 需要在 componentWillUnmount 執行的內容
}
}, [])
~~~
* `componentDidCatch`and`getDerivedStateFromError`:目前**還沒有**這些方法的 Hook 等價寫法,但很快會加上。
為方便記憶,大致匯總成表格如下。
| class 組件 | Hooks 組件 |
| --- | --- |
| constructor | useState |
| getDerivedStateFromProps | useState 里面 update 函數 |
| shouldComponentUpdate | useMemo |
| render | 函數本身 |
| componentDidMount | useEffect |
| componentDidUpdate | useEffect |
| componentWillUnmount | useEffect 里面返回的函數 |
| componentDidCatch | 無 |
| getDerivedStateFromError | 無 |
## 二、單個組件的生命周期
1. 生命周期
V16.3 之前
我們可以將生命周期分為三個階段:
* 掛載階段
* 組件更新階段
* 卸載階段
分開來講:
1. 掛載階段
* `constructor`:避免將 props 的值復制給 state
* `componentWillMount`
* `render`:react 最重要的步驟,創建虛擬 dom,進行 diff 算法,更新 dom 樹都在此進行
* `componentDidMount`
2. 組件更新階段
* `componentWillReceiveProps`
* `shouldComponentUpdate`
* `componentWillUpdate`
* `render`
* `componentDidUpdate`
3. 卸載階段
* `componentWillUnMount`

這種生命周期會存在一個問題,那就是當更新復雜組件的最上層組件時,調用棧會很長,如果在進行復雜的操作時,就可能長時間阻塞主線程,帶來不好的用戶體驗,**Fiber**就是為了解決該問題而生。
V16.3 之后
**Fiber 本質上是一個虛擬的堆棧幀,新的調度器會按照優先級自由調度這些幀,從而將之前的同步渲染改成了異步渲染,在不影響體驗的情況下去分段計算更新。**
對于異步渲染,分為兩階段:
* `reconciliation`:
* `componentWillMount`
* `componentWillReceiveProps`
* `shouldConmponentUpdate`
* `componentWillUpdate`
* `commit`
* `componentDidMount`
* `componentDidUpdate`
其中,`reconciliation`階段是可以被打斷的,所以`reconcilation`階段執行的函數就會出現多次調用的情況,顯然,這是不合理的。
所以 V16.3 引入了新的 API 來解決這個問題:
1. `static getDerivedStateFromProps`:?該函數在**掛載階段和組件更新階段**都會執行,即**每次獲取新的`props`或`state`之后都會被執行**,**在掛載階段用來代替`componentWillMount`**;在組件更新階段配合`componentDidUpdate`,可以覆蓋`componentWillReceiveProps`的所有用法。
同時它是一個靜態函數,所以函數體內不能訪問`this`,會根據`nextProps`和`prevState`計算出預期的狀態改變,返回結果會被送給`setState`**,**返回`null`則說明不需要更新`state`,并且這個返回是**必須的**。
2. `getSnapshotBeforeUpdate`: 該函數會在?**`render`?之后, DOM 更新前**被調用,用于讀取最新的 DOM 數據。
返回一個值,**作為`componentDidUpdate`的第三個參數**;配合`componentDidUpdate`, 可以覆蓋`componentWillUpdate`的所有用法。
注意:V16.3 中只用在組件掛載或組件`props`更新過程才會調用,即如果是因為自身 setState 引發或者forceUpdate 引發,而不是由父組件引發的話,那么`static getDerivedStateFromProps`也不會被調用,在 V16.4 中更正為都調用。
即更新后的生命周期為:
1. 掛載階段
* `constructor`
* `static getDerivedStateFromProps`
* `render`
* `componentDidMount`
2. 更新階段
* `static getDerivedStateFromProps`
* `shouldComponentUpdate`
* `render`
* `getSnapshotBeforeUpdate`
* `componentDidUpdate`
3. 卸載階段
* `componentWillUnmount`

2. 生命周期,誤區
**誤解一:**`getDerivedStateFromProps`?和?`componentWillReceiveProps`?只會在`props`**改變**時才會調用
實際上,**只要父級重新渲染,`getDerivedStateFromProps`?和?`componentWillReceiveProps`?都會重新調用,不管`props`有沒有變化**。所以,在這兩個方法內直接將 props 賦值到 state 是不安全的。
~~~js
// 子組件
class PhoneInput extends Component {
state = { phone: this.props.phone };
handleChange = e => {
this.setState({ phone: e.target.value });
};
render() {
const { phone } = this.state;
return <input onChange={this.handleChange} value={phone} />;
}
componentWillReceiveProps(nextProps) {
// 不要這樣做。
// 這會覆蓋掉之前所有的組件內 state 更新!
this.setState({ phone: nextProps.phone });
}
}
// 父組件
class App extends Component {
constructor() {
super();
this.state = {
count: 0
};
}
componentDidMount() {
// 使用了 setInterval,
// 每秒鐘都會更新一下 state.count
// 這將導致 App 每秒鐘重新渲染一次
this.interval = setInterval(
() =>
this.setState(prevState => ({
count: prevState.count + 1
})),
1000
);
}
componentWillUnmount() {
clearInterval(this.interval);
}
render() {
return (
<>
<p>
Start editing to see some magic happen :)
</p>
<PhoneInput phone='call me!' />
<p>
This component will re-render every second. Each time it renders, the
text you type will be reset. This illustrates a derived state
anti-pattern.
</p>
</>
);
}
}
~~~
[實例可點擊這里查看](https://stackblitz.com/edit/react-yammav)
當然,我們可以在 父組件App 中`shouldComponentUpdate`比較?props 的 email 是不是修改再決定要不要重新渲染,但是如果子組件接受多個 props(較為復雜),就很難處理,而且`shouldComponentUpdate`主要是用來性能提升的,不推薦開發者操作`shouldComponetUpdate`(可以使用`React.PureComponet`)。
我們也可以使用**在 props 變化后修改 state**。
~~~js
class PhoneInput extends Component {
state = {
phone: this.props.phone
};
componentWillReceiveProps(nextProps) {
// 只要 props.phone 改變,就改變 state
if (nextProps.phone !== this.props.phone) {
this.setState({
phone: nextProps.phone
});
}
}
// ...
}
~~~
但這種也會導致一個問題,當 props 較為復雜時,props 與 state 的關系不好控制,可能導致問題
解決方案一:**完全可控的組件**
~~~js
function PhoneInput(props) {
return <input onChange={props.onChange} value={props.phone} />;
}
~~~
**完全由 props 控制,不派生 state**
解決方案二:**有 key 的非可控組件**
~~~js
class PhoneInput extends Component {
state = { phone: this.props.defaultPhone };
handleChange = event => {
this.setState({ phone: event.target.value });
};
render() {
return <input onChange={this.handleChange} value={this.state.phone} />;
}
}
<PhoneInput
defaultPhone={this.props.user.phone}
key={this.props.user.id}
/>
~~~
當?`key`?變化時, React 會**創建一個新的而不是更新一個既有的組件**
**誤解二**:將 props 的值直接復制給 state
**應避免將 props 的值復制給 state**
~~~js
constructor(props) {
super(props);
// 千萬不要這樣做
// 直接用 props,保證單一數據源
this.state = { phone: props.phone };
}
~~~
## 三、多個組件的執行順序
1. 父子組件
* **掛載階段**
分**兩個**階段:
* 第**一**階段,由父組件開始執行到自身的`render`,解析其下有哪些子組件需要渲染,并對其中**同步的子組件**進行創建,按**遞歸順序**挨個執行各個子組件至`render`,生成到父子組件對應的 Virtual DOM 樹,并 commit 到 DOM。
* 第**二**階段,此時 DOM 節點已經生成完畢,組件掛載完成,開始后續流程。先依次觸發同步子組件各自的`componentDidMount`,最后觸發父組件的。
**注意**:如果父組件中包含異步子組件,則會在父組件掛載完成后被創建。
所以執行順序是:
父組件 getDerivedStateFromProps —> 同步子組件 getDerivedStateFromProps —> 同步子組件 componentDidMount —> 父組件 componentDidMount —> 異步子組件 getDerivedStateFromProps —> 異步子組件 componentDidMount
* **更新階段**
**React 的設計遵循單向數據流模型**,也就是說,數據均是由父組件流向子組件。
* 第**一**階段,由父組件開始,執行
1. `static getDerivedStateFromProps`
2. `shouldComponentUpdate`
更新到自身的`render`,解析其下有哪些子組件需要渲染,并對**子組件**進行創建,按**遞歸順序**挨個執行各個子組件至`render`,生成到父子組件對應的 Virtual DOM 樹,并與已有的 Virtual DOM 樹 比較,計算出**Virtual DOM 真正變化的部分**,并只針對該部分進行的原生DOM操作。
* 第**二**階段,此時 DOM 節點已經生成完畢,組件掛載完成,開始后續流程。先依次觸發同步子組件以下函數,最后觸發父組件的。
1. `getSnapshotBeforeUpdate()`
2. `componentDidUpdate()`
React 會按照上面的順序依次執行這些函數,每個函數都是各個子組件的先執行,然后才是父組件的執行。
所以執行順序是:
父組件 getDerivedStateFromProps —> 父組件 shouldComponentUpdate —> 子組件 getDerivedStateFromProps —> 子組件 shouldComponentUpdate —> 子組件 getSnapshotBeforeUpdate —> 父組件 getSnapshotBeforeUpdate —> 子組件 componentDidUpdate —> 父組件 componentDidUpdate
* **卸載階段**
`componentWillUnmount()`,順序為**父組件的先執行,子組件按照在 JSX 中定義的順序依次執行各自的方法**。
**注意**:如果卸載舊組件的同時伴隨有新組件的創建,新組件會先被創建并執行完`render`,然后卸載不需要的舊組件,最后新組件執行掛載完成的回調。
2. 兄弟組件
* **掛載階段**
若是同步路由,它們的創建順序和其在共同父組件中定義的先后順序是**一致**的。
若是異步路由,它們的創建順序和 js 加載完成的順序一致。
* **更新階段、卸載階段**
兄弟節點之間的通信主要是經過父組件(Redux 和 Context 也是通過改變父組件傳遞下來的?`props`?實現的),**滿足React 的設計遵循單向數據流模型**,**因此任何兩個組件之間的通信,本質上都可以歸結為父子組件更新的情況**。
所以,兄弟組件更新、卸載階段,請參考**父子組件**。
走在最后:走心推薦一個在線編輯工具:[StackBlitz](https://stackblitz.com/),可以在線編輯 Angular、React、TypeScript、RxJS、Ionic、Svelte項目
## 摘自
[ 你真的了解 React 生命周期嗎?](https://github.com/sisterAn/blog/blob/master/articles/React%E7%B3%BB%E5%88%97/Hooks%20%E4%B8%8E%20React%20%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F%E7%9A%84%E5%85%B3%E7%B3%BB.md)