# Counter計數器組件
通過計數器組件來實現數字增加和減少。
## 涉及知識點
>綁定事件,this指向解決,生命周期。內容量還是比較多的
## 新建Counter組件
`src/components/Counter.js`
~~~jsx
import React, {Component} from 'react'
export default class Counter extends Component {
constructor(props) {
super(props)
this.state = {num: 0}
}
icrement() {
this.setState({
num: this.state.num + 1
})
}
render() {
return <div>
<span id="span">{this.state.num}</span>
<br/>
<button onClick={this.icrement.bind(this)}>增加</button>
</div>
}
}
~~~
# 問題
1.在這里面要注意數據更新必須是通過**this.setState()**來進行更新數據,
不可以直接對數據進行this.state.num++這種前置++或者后置++來實現效果。因為this.state.num++只會影響數據增加,但不會更新視圖。
this.setState()其實和微信小程序的操作方式很相似。
2.綁定事件,綁定事件要注意,這里要通過onClick= {this.increment}這樣寫會報錯,this指向有問題。打印后this是undefined。必須手動修改this指向。通過this.increment.bind(this);
為什么不是call 和applay,而是bind。答案很簡單,call和applay調用的話會立即執行。而bind 不會立即執行它是call和applay的衍生版。
下面是bind的簡易版實現方式。
~~~
function bind(fn,context){
return function(){
return fn.apply(context,arguments)
}
}
~~~
如果想看復雜版請查看[Bind高級版](https://www.jianshu.com/p/4b293581a03f)
# 正式進入生命周期
## 初始化階段
### `constructor`初始化數據
~~~
console.log('這是constructor')
console.log(document.getElementById('box'))
this.setState({
num:1
})
~~~
打印結果
```
這是constructor
null
```
通過打印結果說明網頁初始化發布時,默認會先執行constructor生命周期,在這時候還不能獲取到真實的dom元素。
### `componentWillMount`將要被掛載
~~~
componentWillMount() {
console.log('這是componentWillMount',2)
console.log(document.getElementById('box'))
}
~~~
打印結果
```
這是componentWillMount 2
null
```
通過打印結果說明網頁初始化發布時,第二個執行componentWillMount生命周期,這時候還不能獲取到真實的dom元素。
### render生命周期 進行渲染
因為初始化,它會先將數據和結構進行綁定,然后轉換為虛擬dom,此時不需要domdiff算法,直接將數據呈現在網頁中即可。
~~~jsx
render() {
console.log('這是render',3)
console.log(document.getElementById('box'))
return <div id="box">
<span id="span">{this.state.num}</span>
<br/>
<button onClick={this.icrement}>增加</button>
</div>
}
~~~
打印結果
```
這是render 3
null
```
通過打印結果說明網頁初始化發布時,第三個執行render 生命周期,這時候還不能獲取到真實的dom元素。
### `componentDidMount`掛載完成
~~~
componentDidMount() {
console.log('這是componentDidMount',4)
console.log(document.getElementById('box'))
}
~~~
```
這是componentDidMount 4
<div id=?"box">?…?</div>?
```
通過打印結果說明,網頁初始化發布是,第四個執行componentDidMount 生命周期,這時候數據和模板已經更新完成,可以獲取真正的元素了。
#初始化生命周期小結
通過上面我們已經可以得到什么時候可以去獲取真實的dom元素以及它們的執行順序。
# 運行中生命周期
運行中生命周期分兩種情況,可以根據下圖來說明,第一種是私有數據發生改變,第二種是屬性發生改變。

# 我們先研究私有數據即state發生改變的情況。
## shouldComponentUpdate 組件是否更新生命周期
~~~
shouldComponentUpdate(nextProps, nextState, nextContext) {
console.log('shouldComponentUpdate',5)
console.log(document.getElementById('box').innerHTML)
}
~~~
單擊增加按鈕,我們讓state中的num值變為10,控制臺會輸出下面內容。
```
shouldComponentUpdate 5
<span id="span">0</span><br><button>增加</button>
Warning: Counter.shouldComponentUpdate(): Returned undefined instead of a boolean value. Make sure to return true or false.
```
控制臺輸出說明,它默認不會自動觸發,當私有數據發生變化時才會更新,同樣的也能獲取到標簽元素,不過注意這個標簽元素是老的數據,并不是最新的。
我們也發現瀏覽器中并未將數據更新為10。控制臺還報錯了:我們必須在這個生命周期中返回true或者false。返回true表示會更新,如果返回false則不更新。
可以嘗試自己寫寫試試效果
### 小結
`shouldComponentUpdate`生命周期,一般我們可以用來優化性能,需要人為來處理,一般對于基礎數據我們可以來進行性能優化,如果是比較復雜的對象或者數組則不需要,讓它在`render`中進行優化。
## componentWillUpdate
~~~
componentWillUpdate(nextProps, nextState, nextContext) {
console.log('componentWillUpdate',6)
console.log(document.getElementById('box').innerHTML)
}
~~~
控制臺輸出
```
componentWillUpdate 6
<span id="span">0</span><br><button>增加</button>
```
根據控制臺輸出結果我們可以看到組件將要被更新,且能獲取到元素,且元素的內容還是老舊內容。
## render生命周期函數
render生命周期函數改造
在此時,這個生命周期函數做的事情較為復雜,內部需要對比老舊的虛擬dom,進行diff算法。
~~~
console.log('這是render',3)
console.log(document.getElementById('box')&&document.getElementById('box').innerHTML)
~~~
控制臺輸出
```
這是render 3
Counter.js:65 <span id="span">0</span><br><button>增加</button>
```
根據控制臺輸出,表明render生命周期還是之前那一個,同樣的,它獲取的元素內容還是之前老舊的內容。
## componentDidUpdate生命周期
~~~
componentDidUpdate(prevProps, prevState, snapshot) {
console.log('componentDidUpdate',7)
console.log(document.getElementById('box').innerHTML)
}
~~~
控制臺輸出
```
componentDidUpdate 7
<span id="span">1</span><br><button>增加</button>
```
這時我們它是運行中生命周期中最后一個,同時,完成更新后,它獲取的元素內容已經成為最新的了。
## 運行中生命周期小結
運行中生命周期中是組件持續時間最長的。只要觸發this.setState這個函數它就會執行。
大家可以猜想一下為什么在初始化render生命周期不可以調用this.setState可以直接告訴大家它是死循環,可以將你的思考總結一下(了解一下)**,這個題需要看完思考題 再來解釋。**
# 組件銷毀生命周期(了解掌握)
組件銷毀生命周期這個案例比較復雜,在理解上有點麻煩。大家剛開始時候只需要能跟著寫就可以了。在這里我們需要再學習兩個ReactDOM的api。
通過ReactDOM的unmountComponentAtNode來卸載組件。不過這里卸載組件只能卸載通過ReactDOM.render加載的組件。
~~~
ReactDom.unmountComponentAtNode(document.getElementById('box'))
銷毀指定容器內的所有React節點。
~~~
我們需要結合Counter計數組器組件和Main.js中添加一層組件Life組件。
## 新建Life.js組件
~~~
import React, {Component, Fragment} from 'react'
import ReactDom from 'react-dom'
import Counter from "./Counter";
export default class Life extends Component{
unmont(){
ReactDom.unmountComponentAtNode(document.getElementById('mybox'))
}
render() {
return (
<Fragment>
<button onClick={this.unmont.bind(this)}>卸載</button>
<div id="mybox">
<Counter/>
</div>
</Fragment>
)
}
}
~~~
### 修改main.js
~~~
ReactDom.render(<Life/>,document.getElementById('root'))
~~~

##嘗試卸載
單擊卸載按鈕報錯
```
Warning: unmountComponentAtNode(): The node you're attempting to unmount was rendered by React and is not a top-level container. You may have accidentally passed in a React root node instead of its container.
```
報錯信息,說的不通過這種形式卸載組件。
## 解決問題
我們還記得可以卸載的組件只能通過通過ReactDOM.render()的組件才可以卸載掉。
~~~
unmont(){
ReactDom.unmountComponentAtNode(document.getElementById('mybox'))
}
mount(){
ReactDom.render(<Counter/>,document.getElementById('mybox'))
}
render() {
return (
<Fragment>
<button onClick={this.mount.bind(this)}>加載</button>
<button onClick={this.unmont.bind(this)}>卸載</button>
<div id="mybox">
</div>
</Fragment>
)
}
~~~
通過上面,我們要通過單擊加載按鈕才可以讓元素顯示出來,同樣的我們單擊卸載,它也可以直接將Counter組件卸載掉。
我們設置Counter.js組件中的卸載生命周期
~~~
componentWillUnmount() {
console.log('componentWillUnmount',8);
console.log(document.getElementById('box').innerHTML)
}
~~~
單擊卸載按鈕,控制臺輸出
```
componentWillUnmount 8
Counter.js:111 <span id="span">0</span><br><button>增加</button>
```
組件卸載生命周期只有一個,但是也比較重要,比如當前頁面被卸載掉后我們經常要使用它來清除定時器,以達到節省性能的效果。
# 運行中生命周期第二種情況
運行中生命周期第二種情況就是當父組件給子組件傳遞數據,引發的改變。
那么LIfe組件中使用了Counter組件,那么在Life組件中我們可以去改變Counter組件的數據。
## Life組件
~~~
import React, {Component, Fragment} from 'react'
import ReactDom from 'react-dom'
import Counter from "./Counter";
export default class Life extends Component{
constructor(props){
super(props);
this.state = {
number:10
}
}
unmont(){
ReactDom.unmountComponentAtNode(document.getElementById('mybox'))
}
mount(){
ReactDom.render(<Counter number={this.state.number}/>,document.getElementById('mybox'))
}
changeState(){
this.setState({
number:100
},()=>{
this.mount();
})
}
render() {
return (
<Fragment>
<button onClick={this.changeState.bind(this)}>修改number</button>
<button onClick={this.mount.bind(this)}>加載</button>
<button onClick={this.unmont.bind(this)}>卸載</button>
<div id="mybox">
</div>
</Fragment>
)
}
}
~~~
## Countr組件
~~~
constructor(props) {
super(props)
this.state = {num: props.number}
console.log('這是constructor,1')
console.log(document.getElementById('box'))
}
componentWillReceiveProps(nextProps, nextContext) {
console.log('componentWillReceiveProps',9)
console.log(document.getElementById('box').innerHTML)
}
~~~
## 解釋
在這里面我們在Life組件中新添加了私有數據,然后級Counter組件的父組件傳遞number數據,讓子組件接收props,然后將props的數據放到Counter組件的state數據的num屬性中
### 單擊加載,再單擊修改number
控制臺輸出結果
```
componentWillReceiveProps 9
<span id="span">10</span><br><button>增加</button>
shouldComponentUpdate 5
<span id="span">10</span><br><button>增加</button>
componentWillUpdate 6
<span id="span">10</span><br><button>增加</button>
這是render 3
<span id="span">10</span><br><button>增加</button>
componentDidUpdate 7
<span id="span">10</span><br><button>增加</button>
```
我們可以看出只要當父組件的屬性的值發生變化 ,那么會引起子組件運行中的所有生命周期會一一執行。不過還可以發現一個問題,我們更新父組件中的number值,子組件并沒有更新成功,原因是因為什么呢?
### 解決
componentWillReceiveProps 這個生命周期我們可以接收到父容器的數據,我們來查看一下。
其實我們應該利用最新的屬性數據來改變私有數據。讓其更新。基本新的私有數據如何獲得呢?我們通過控制臺發現props并不是最新的數據,而nextProps才是最新的數據
~~~
componentWillReceiveProps(nextProps, nextContext) {
console.log(this.props.number)
console.log(nextProps.number)
console.log('componentWillReceiveProps',9)
console.log(document.getElementById('box').innerHTML)
}
~~~

和最新的props數據來更新state中的數據
~~~
componentWillReceiveProps(nextProps, nextContext) {
console.log(this.props.number)
console.log(nextProps.number)
this.setState({
num:nextProps.number
})
console.log('componentWillReceiveProps',9)
console.log(document.getElementById('box').innerHTML)
}
~~~

## 思考題:
但是他們什么時候可以去更新數據呢?也就是this.setState這個函數在生命周期中什么時候可以執行呢?即比如ajax呢?
我們可以在上面的所有生命周期中,都添加`this.setState({num:10})`。
發現如下效果:
### constructor中不能更新數據
~~~
Warning: Can't call setState on a component that is not yet mounted. This is a no-op, but it might indicate a bug in your application. Instead, assign to `this.state` directly or define a `state = {};` class property with the desired state in the Counter component
~~~
>警告:無法對尚未掛載的組件調用SetState。這是一個禁止操作,但它可能指示應用程序中的錯誤。相反,直接分配給“this.state”,或在計數器組件中定義一個具有所需狀態的“state=”類屬性。
### render,shouldComponentUpdate,componentWillUpdate,componentDidUpdate不能更新數據錯誤原因一樣
~~~
Uncaught Invariant Violation: Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.
~~~
超出最大更新深度。當組件在componentWillUpdate或componentDidUpdate中重復調用setState時,可能會發生這種情況。React限制嵌套更新的數量以防止無限循環。
原因是為什么?因為當state發生改變會執行上面的生命周期,自然而然的就導致死循環,最終報錯。
### componentWillMount,componentDidMount,componentWillReceiveProps可以更新數據
這里面可以進行ajax。

總結:React生命周期在學習上成本上是很大的,在剛工始學習過程中會遇到問題,也會感覺到特別麻煩 ,反而沒有Vue的生命周期好用。這些都是正常的,要多練習幾次就可以達到熟練的效果。
- webpack復習
- React基礎
- 前端三大主流框架對比
- React中幾個核心的概念
- React基礎語法
- React JSX語法
- React組件
- 普通組件
- 組件通信-父向子傳遞
- 組件拆成單個文件
- 面向對象復習
- Class組件基礎
- Class組件的私有狀態(私有數據)
- 案例:實現評論列表組件
- 組件樣式管理
- 組件樣式分離-樣式表
- CSS模塊化
- 生命周期
- React組件生命周期
- Counter組件來學習組件生命周期
- 生命周期總結
- 生命周期案例
- React評論列表
- React雙向數據綁定
- React版todolist
- 其它提高(了解)
- 組件默認值和數據類型驗證
- 綁定this并傳參的三種方式
- 祖孫級和非父子組件傳遞數據(了解)
- React路由
- 路由基礎
- 動態路由
- 路由嚴格模式
- 路由導航定位
- 路由重定向
- 路由懶加載
- WolfMovie項目
- 項目初始化
- AntDesign使用
- 其它相關了解