owned this note
owned this note
Published
Linked with GitHub
# React 的生命週期
###### tags: `javascript`、`react`
## 前情提要
對於 React 基本觀念還不熟悉的話,請參考[這篇文章](https://hackmd.io/PRs8nUPPQYG344wnG-ICtg)
## constructor 的作用
下面藉由兩種寫法認識:
1. constructor 的作用與注意事項
2. 介紹在 React 當中,通常如何撰寫 JSX 讓 Component 依據相對應的條件被移除或是新增。
第一種寫法:
```jsx=
import React, { Component } from 'react'
// Children Component
class Title extends Component {
constructor() {
super()
console.log('Title created')
}
render() {
return (
<h1>title</h1>
)
}
}
// Parent Component
class App extends Component {
constructor() {
super()
this.state = {
isShow: true
}
console.log('App created')
}
render() {
const { isShow } = this.state // 使用解構的寫法方便指定 state 的 isShow
return (
<div>
{isShow && <Title />} // 新增與移除 Component 的技巧
// 每次點按鈕就會把 state 的 isShow 設定到當前布林值的相反值
<button onClick={() => {
this.setState({
isShow: !this.state.isShow
})
}}>Toggle</button>
</div>
)
}
}
export default App
```
第二種寫法:
```jsx=
import React, { Component } from 'react'
class Title extends Component {
constructor() {
super()
console.log('Title created')
}
render() {
// 大家在用 React 時很常使用的寫法,方 bang
const { isShow } = this.props
return (
// isShow 是 True 的話就顯示 <h1>;false 的話就隱藏 <h1>
<h1 style={{
display: isShow ? 'block' : 'none'
}}>title</h1>
)
}
}
class App extends Component {
constructor() {
super()
this.state = {
isShow: true
}
console.log('App created')
}
render() {
const { isShow } = this.state
return (
<div>
// 設定 isShow function 讓 chidren component 來呼叫
<Title isShow={isShow} />
<button onClick={() => {
this.setState({
isShow: !this.state.isShow
})
}}>Toggle</button>
</div>
)
}
}
export default App
```
### 先看第一種寫法,來看看它的初始 render UI:
![render](https://i.imgur.com/TIIo965.png)
可以注意到 DevTools 的 Console 首先先印出 `App created` 再來才是 `Title created`,很合理,因為它是 Parent Component,會先被執行,所以理所當然 `constructor` 裡面的 `console.log('App created')`(第 22 行)結果會先被印出來,緊接著才執行 `render()`,碰到 `<Title />` 標籤才執行 `Title` 的 Component,有執行 `Title` 的時候,`Title` 的 `constructor` 才會被執行,進而執行 `console.log('Title created')`。
在程式碼當中我們設定 `Toggle` 字樣的 button 來當作顯示與隱藏 Component 的開關,這個寫法在 React 很常使用,因為在 JSX 語法不能直接寫 `if` 判別式,所以使用短路的方式來寫,也就是第 29 行的 `{isShow && <Title />}` 寫法,只要 `isShow` 是 `false`,那麼後面的 `<Title />` 就不會被顯示出來 and vice versa,因此這樣的做法就可以當作控制 Component 的新增與移除了。
回到這個例子,只要 `title` 字樣存在(也就是 `<h1>title</h1>` 在 DOM 上,表示 `<Title />` 存在於 `App` Component),表示目前的 `isShow` 是 `true`,那麼按了 `Toggle` 之後,`isShow` 就變成 `false`,因此 `<Title />` 這個標籤就不存在於 `App` Component,因此 `Title` Component 的元素就不會呈現在畫面當中,如下圖:
![](https://i.imgur.com/PhVAk4N.png)
### 兩種寫法的差別
實際上兩種寫法呈現的 UI 都是一樣的,所以我在上面僅有詳細介紹第一種寫法的 UI 呈現,但是!沒錯,就是有這個但是,兩種寫法對於 DOM 節點的呈現有所不同。
首先第一種寫法的 `<Title />` 標籤,存在於 `App` Component 與否必須根據 `isShow` 的值決定,當今天 `isShow` 是 `true`,它的 DOM 是長這樣的:
![](https://i.imgur.com/lHhJKyL.png)
`isShow` 是 `false` 的 DOM:
![](https://i.imgur.com/85uNL5I.png)
很明顯的,當 `isShow` 為 `false` 的時候,`<h1>title</h1>` 這個 DOM 節點完全地消失於 DOM 上面,表示說 `<Title />` 這個標籤已經不在 `App` Component 裡面了。
而第二種寫法是 `<Title />` 這個標籤不管 UI 有沒有呈現相對應的 `Title` Component 的元素, `<Title />` 都會一直存在於 `App` Component 裡面,這代表什麼意思?就是整個 DOM 會一直存在 `<Title />` 的元素節點,它只不過是改變了 CSS 屬性所以導致沒顯示在畫面上而已,下面的兩張圖片我們可以分別查看 UI 沒有 `title` 字串和有 `title` 字串時,DOM 的差別:
畫面有 `title` 字串:
![](https://i.imgur.com/5lAZrpl.png)
畫面沒有 `title` 字串:
![](https://i.imgur.com/y3IeKtF.png)
顯而易見的是,`<h1>` 這個 DOM 節點一直存在著,只不過是 `display` 的值改變而已。
### 與 constructor 的關係
今天如果使用第一種寫法,我們先去看 DevTools 的 Console:
這是一開始 render log 出來的字串並且先隱藏了 `title`:
![](https://i.imgur.com/qEj8Osg.png)
當再按一次 `Toggle`:
![](https://i.imgur.com/CpcBr8k.png)
會發現到多了一次 `Title created` 的 log,這代表什麼?表示說每當 `<Title />` 標籤從 `App` Component 當中連根拔除又把 `<Title />` 塞回去後,`Title` Component 的 `constructor` 就會被重新執行一次,在這個例子當中就是 `console.log('Title created')` 又被執行一次了,也就是說每次 Component 在 UI 被(重新)建立時,Component 的 `constructor` 這個 method 都會被執行,而且是先被執行,接著才是後續的其它 method(諸如 `render`)被執行。另外`App` Component 的 `constructor` 在這個例子當中,只執行了第一次,因為就只有 `export` `App` 的時候,`App` Component 被建立一次而已。
### 更正確的 constructor 寫法
拿上面的 `Title` Component 來舉例:
```jsx=
class Title extends Component {
constructor() {
super()
console.log('Title created')
}
render() {
return (
<h1>title</h1>
)
}
}
```
原本我們的 `constructor` 都是這麼寫的,不過使用了 `constructor` 通常就是會需要使用到 `props`,因此最保險的做法其實應該在有使用 `constructor` 的地方都帶入 `props` 參數,也就是這樣:
```jsx=
class Title extends Component {
constructor(props) {
super(props)
console.log('Title created')
}
render() {
return (
<h1>title</h1>
)
}
}
```
這樣的寫法會是比較不會出什麼紕漏的寫法,否則會跑出莫名其妙的 bug。
如果 `class` 當中不需要使用 `construcotr` 的話(比如不需要 `props`),不需要建立 `construcotr` 也是可以的。
## shouldComponentUpdate
這個單元我們以下面的程式碼作為範例:
```jsx=
class App extends Component {
constructor(props) {
super(props)
this.state = {
number: 1
}
}
render() {
const { number } = this.state
return (
<div>
<h1>{number}</h1>
<div onClick={() => {
this.setState({
number: this.state.number + 1
})
}}>Click Me</div>
</div>
)
}
}
```
介面長這樣:
![](https://i.imgur.com/MS2IWpu.png)
只要按了 `Click Me`,`state.numer` 就會被加 1,然後重新 render UI,而這整個過程就是 React 的基本用法,`state` 被改變了之後,會自動 call `render` 把 UI 重新渲染一遍呈現更新過後的畫面。
而 `state` 改變之後自動 call `render` 的運作方式實際上和 `shouldComponentUpdate` 這個 method 有關係,它的預設值是 `true`:
```javascript=
shouldComponentUpdate(nextProps, nextState) {
return true
}
```
如果把它改為 `return false` 的話,那麼 `state` 改變就不會 call `render` 了,也就是不會更新 UI 的畫面,不過 `state` 的值依然有確實地被改變的。
也可以在 `shouldComponentUpdate` method 當中寫入判別式:
```javascript=
shouldComponentUpdate(nextProps, nextState) {
if (nextState.number === 3) {
return false
}
return true
}
```
這樣的意思就是當 `state.number` 的值變為 3 的時候就不會 call `render`,其它 `state.number` 數值則照常 call `render`。
### 實際的例子
```jsx=
class Title extends Component {
render() {
console.log('Title render')
return (
<h1>{this.props.title}</h1>
)
}
}
class App extends Component {
constructor(props) {
super(props)
this.state = {
number: 1
}
}
render() {
const { number } = this.state
return (
<div>
<h1>{number}</h1>
<Title title={'hello world'} />
<div onClick={() => {
this.setState({
number: this.state.number + 1
})
}}>Click Me</div>
</div>
)
}
}
```
上面的例子畫面長這樣:
![](https://i.imgur.com/T35s3SL.png)
當每次按 `Click Me` 的時候, `1` 的數字會跟著 `state.number` 改變而加 1,而 `Title` 這個 Component 也會跟著重新 `render()` ,儘管 `Title` 的內容完全沒變化(一直都是 `hello world`):
![](https://i.imgur.com/OroC8CB.png)
可以看到在第 3 行寫的 `console.log('Title render')` 一直被 log,也就是每按一下 `Click Me` 就被 log 一次,也就是說 `Title` Component 一直觸發 `render`。
但是我們不想要這樣,我們希望內容沒改變的 Component 就不要再 `render` 一遍,這時就可以派出我們的得力助手:`shouldComponentUpdate`。
我們可以在 `Title` Component 加上這段 `shouldComponentUpadte`:
```jsx=
class Title extends Component {
shouldComponentUpdate(nextProps) {
if (nextProps.title !== this.props.title) {
return true
}
return false
}
render() {
console.log('Title render')
return (
<h1>{this.props.title}</h1>
)
}
}
```
意思就是當今天下一個收到的 `props` 的 `title` 的值和目前的 `props` 的 `title` 值不一樣的時候,才需要做 `render`(`true`),否則平常時候就不做 `render`(`false`)。
而使用 `shouldComponentUpdate` 的目的當然是為了減少不必要的 `render` 進而拖慢效能,但這樣子做是否就都可以達到效能優化了?答案是未必,得端看 `shouldComponentUpdate` 的整體執行速度,主要是程式碼第 3 行 `if (nextProps.title !== this.props.title)` 這其中的判別式處理的速度和 `render` 一次的速度究竟哪個比較快,所以不是說使用越多 `shouldComponentUpdate` 來減少 `render` ,效能就比較好,除非今天要 `render` 的內容實在多到一個不行那就顯而易見了。
另外 `shouldComponentUpdate` 的設定也會影響到下面有講到的 `componentDidUpdate`,如果是 `shouldComponentUpdate` 是設定 `false`,那麼 `componentDidUpdate` 也不會有作用。
## componentDidMount 與 componentWillUnmount
以這個為例子:
```jsx=
class Title extends Component {
render() {
return (
<h1>hello </h1>
)
}
}
class App extends Component {
constructor(props) {
super(props)
this.state = {
showTitle: true
}
}
render() {
const { showTitle } = this.state
return (
<div>
<div onClick={() => {
this.setState({
showTitle: !this.state.showTitle
})
}}>toggle</div>
{showTitle && <Title />}
</div>
)
}
}
```
UI 的畫面:
![](https://i.imgur.com/imic3xG.png)
當點擊 `toggle` 的時候,`hello` 有關的 DOM 就會消失或者是出現。
接著正式進入這一個主題。在 Component call `render` 之後的某一段時間必須經過一個動作叫「Mount」,所有 `render` 的內容才會被真正放到 DOM 上面,而與 `componentDidMount` 這個 method 有關,經過這個 method 當中的所有元素與動作其實都是已經被放到 DOM 上面了,換句話說,在 `componentDidMount` 裡面,你必定可以選到任何 `render` 後的內容。我們把上面的例子的三處地方新增內容:
```jsx=
class Title extends Component {
render() {
return (
<h1>hello </h1>
)
}
}
class App extends Component {
constructor(props) {
super(props)
this.state = {
showTitle: true
}
// update here
console.log('created', document.querySelector('.test'))
}
// update here
componentDidMount() {
console.log('Mount', document.querySelector('.test'))
}
render() {
const { showTitle } = this.state
return (
< div >
// update here
<div className='test' onClick={() => {
this.setState({
showTitle: !this.state.showTitle
})
}}>toggle</div>
{showTitle && <Title />}
</div >
)
}
}
```
我們在 `toggle` 的 DOM 上新增 `class='test'`,然後分別在 Component 被建立時,和在 `componentDidMount` 裡面,選取 `class='test'` 這個 DOM 節點,結果發現到只有在 `componentDidMount` 才選取得到這個節點:
![](https://i.imgur.com/KhTJw1z.png)
也就是說,在 Component 建立的過程,DOM 節點其實還未被放到畫面上;另外一方面,我們在 `componentDidMount` 那邊可以確定百分之百拿得到 `render` 後的 DOM 節點。
**而 `componentDidMount` 比較大的作用是可以做一些初始化的設定**,比方說:
```jsx=
class Title extends Component {
constructor(props) {
super(props)
this.state = {
title: 'hello'
}
}
componentDidMount() {
setTimeout(() => {
this.setState({
title: 'yoyoyo!'
})
}, 2000)
}
render() {
return (
<h1>{this.state.title} </h1>
)
}
}
class App extends Component {
constructor(props) {
super(props)
this.state = {
showTitle: true
}
}
render() {
const { showTitle } = this.state
return (
< div >
<div onClick={() => {
this.setState({
showTitle: !this.state.showTitle
})
}}>toggle</div>
{showTitle && <Title />}
</div >
)
}
}
```
我們在 `Title` Component 裡面設定一個 `componentDidMount`,這個 `componentDidMount` 設定了一個 `setTimeout()`,過了 2 秒之後,`state` 的 `title` 值就會由 `hello` 被改變成 `yoyoyo!`。
不過,雖然上面使用 `componentDidMount` 達到初始化的設定了,但卻衍伸出另外一個問題,當我們按了 `toggle` 後,`Title` Component 就會從 DOM 節點上消失,兩秒後就會啟動 `setState` 的功能,但在 DOM 上面卻找不到 `Title` Component 的節點因此這個 `state` 的更新就會顯示這樣的錯誤:
![](https://i.imgur.com/qpZs3U4.png)
而這時我們需要使用另外一個 method 避免這個錯誤發生,也就是 `componentWillUnmount`, `componentWillUnmount` 如名稱所述,它是一個在 component 要被「unmount」以前,會早一步觸發它裡面所寫的功能的 method,因此,我們要解決上面的錯誤的話,可以這麼寫:
```jsx=
// 這邊只貼上有修改與新增的段落
componentDidMount() {
this.timer = setTimeout(() => {
this.setState({
title: 'yoyoyo!'
})
}, 2000)
}
componentWillUnmount() {
clearTimeout(this.timer)
}
```
為 `componentDidMount` 加上 `this.timer` 去指定 `setTimeout`,接著新增 `componentWillUnmount`,然後在裡面新增 `clearTimeout(this.timer)`,也就是清除 `this.timer` 這個計時器,因此現在只要 `Title` Component 要被 unmount 以前,就會去觸發`componentWillUnmount` 讓它清除計時器。
也因此 `componentDidMount` 和 `componentWillUnmount` 通常是成雙成對的,兩個通常會同時設定,不然可能會發生不必要的錯誤。
## componentDidUpdate
`componentDidUpdate` 是用在當 Component 的 `state` 更新的時候會被 call 的 method。
以下面這個例子為例:
```jsx=
class App extends Component {
constructor(props) {
super(props)
this.state = {
apple: 1,
orange: 1,
banana: 1,
}
this.addApple = this.addApple.bind(this)
this.addOrange = this.addOrange.bind(this)
this.addBanana = this.addBanana.bind(this)
}
addApple() {
this.setState({
apple: this.state.apple + 1
})
}
addOrange() {
this.setState({
orange: this.state.orange + 1
})
}
addBanana() {
this.setState({
banana: this.state.banana + 1
})
}
render() {
const { apple, orange, banana } = this.state
return (
<div>
<div>
apple: {apple}
<button onClick={this.addApple}>+1</button>
</div>
<div>
orange: {orange}
<button onClick={this.addOrange}>+1</button>
</div>
<div>
banana: {banana}
<button onClick={this.addBanana}>+1</button>
</div>
</div>
)
}
}
```
這個例子的 UI 介面長這樣:
![](https://i.imgur.com/X7QvHnh.png)
這個範例是,當按了各別水果的 `+1` button 後,各別水果的數量就會增加 1 個。
如果今天我們想要 log 出每個水果在更新數量之後,顯示當前的水果數量的話,可以這麼寫:
```jsx=
addApple() {
this.setState({
apple: this.state.apple + 1
})
console.log(this.state.apple, this.state.orange, this.state.banana)
}
addOrange() {
this.setState({
orange: this.state.orange + 1
})
console.log(this.state.apple, this.state.orange, this.state.banana)
}
addBanana() {
this.setState({
banana: this.state.banana + 1
})
console.log(this.state.apple, this.state.orange, this.state.banana)
}
```
替三種水果變更 `state` 的 method 當中放入 `console.log()`,可是這樣很沒效率,我們可以改這麼寫:
```jsx=
log() {
console.log(this.state.apple, this.state.orange, this.state.banana)
}
addApple() {
this.setState({
apple: this.state.apple + 1
})
this.log()
}
addOrange() {
this.setState({
orange: this.state.orange + 1
})
this.log()
}
addBanana() {
this.setState({
banana: this.state.banana + 1
})
this.log()
}
```
新增一個 `log` 的 method,裡面放 `console.log()`,這樣的話,在各個變更 `state` 的 method 只要都呼叫 `this.log` 就比較簡潔並且好維護了,可是會發現到一個一開始就存在的問題:
![](https://i.imgur.com/wdk0qdr.png)
當按了按鈕新增蘋果的數量之後,`console.log()` 出來的數量卻沒有新增,這就意味著,當 `setState` 執行完之後,並不會馬上執行 `console.log()`,也就是說,這兩行指令是非同步的。如果要解決這個問題,其實在 `setState` 函式中還有第二個參數,就是 callback function,也就是說當執行完 `setState` 才會 call 第二個參數的 function 執行,因此我們只要把 `this.log` 放進去 `setState` 就可以了,像這樣:
```jsx=
log() {
console.log(this.state.apple, this.state.orange, this.state.banana)
}
addApple() {
this.setState({
apple: this.state.apple + 1
}, () => {
this.log()
})
}
addOrange() {
this.setState({
orange: this.state.orange + 1
}, () => {
this.log()
})
}
addBanana() {
this.setState({
banana: this.state.banana + 1
}, () => {
this.log()
})
}
```
或者直接放 `this.log` function 在第二個參數即可。
上面的作法是當對 `state` 做操作的時候我們就去 log 結果出來,不過我們可以換另一個角度來看,如果今天我們的判定方式改為當 Component 的 `state` 有被改變(Update)時就去 log 的話,寫起來會比較好維護,而這就要用到 `componentDidUpdate` 來實作了,所以我們可以這麼改:
```jsx=
componentDidUpdate(prevProps, prevState) {
if (prevState.apple !== this.state.apple
|| prevState.orange !== this.state.orange
|| prevState.banana !== this.state.banana
) {
this.log()
}
}
log() {
console.log(this.state.apple, this.state.orange, this.state.banana)
}
addApple() {
this.setState({
apple: this.state.apple + 1
})
}
addOrange() {
this.setState({
orange: this.state.orange + 1
})
}
addBanana() {
this.setState({
banana: this.state.banana + 1
})
}
```
這樣的好處是,所有關於 `log` 的語法都會被集中在 `componentDidUpate`,也因此不會說發生忘了加上其中一個 `log` 在某一個水果上的問題,同時維護性變得更好,我們只需要單獨更動 `componentDidUpate` 裡面的內容即可。
## 其它 lifecycle
在這篇文章當中,所有提到的新名詞都是 React 的 lifecycle 相關語法,不過除了這些 lifecycle,還有兩個比較少在用的 lifecycle 沒被提到,可以到 React 官網的這邊來看:http://projects.wojtekmaj.pl/react-lifecycle-methods-diagram/
而且上面網址的 diagram 集結了所有 lifecycle 會在 UI 呈現的流程的哪邊運作,還滿清楚的。