前言
組件中的state具體是什么?怎么更改state的數據?
setState函數分別接收對象以及函數有什么區別?
如何劃分組件的狀態數據,進行自我的靈魂拷問,以及props與state的靈魂對比
那么本節就是你想要知道的
React中的state
一個組件最終渲染的數據結果,除了prop還有state,state代表的是當前組件的內部狀態,你可以把組件看成一個'狀態機",它是能夠隨著時間變化的數據,更多的是應當在實現交互時使用,根據狀態state的改變呈現不同的UI展示
在React中,因為不能直接修改外部組件傳入的prop值
當需要記錄組件自身數據變化時,想要使組件具備交互的能力,那么需要有觸發該組件基礎數據模型改變的能力,那么此時就需要使用state
一旦組件的狀態(數據)發生更改,組件就會自動的調用render函數重新渲染UI,更改這個state狀態是通過this.setState方法來觸發實現的
下面我們從一個簡單的點擊按鈕,顯示和隱藏的效果開始: 效果如下所示:
連續點擊按鈕,上方的itclanCoder文本在顯示和隱藏進行切換,當狀態為true時,itclanCoder文本顯示,狀態為false時,itclanCoder文本隱藏,注意控制臺調試器
具體代碼如下所示:
import React, { Fragment, Component } from 'react';
import ReactDOM from 'react-dom';
import './index.css';
class Button extends Component {
constructor(props){
super(props);
// 初始化state
this.state = {
isShow: true
}
}
render(){
return(
<div style = {{ textAlign: "center"}}>
<div className = { this.state.isShow?"showText":"hiddenText" }>itclanCoder</div>
<div>{ String(this.state.isShow) }</div>
<div><button onClick = { this.handleBtnClick }>點擊按鈕切換文本狀態</button></div>
</div>
);
}
handleBtnClick = () => {
this.setState({
isShow: !this.state.isShow
});
}
}
const container = document.getElementById('root');
ReactDOM.render(<Button />, container);
通過上面的代碼,可以看出初始化state數據,一般在組件的構造器結尾處進行編寫
在上面的Button組件內,通過對this.state的賦值,完成了對該Button組件內部state的初始化
注意:
this.state放置的位置:應當放在構造器函數內進行使用的,否則是會報錯的
初始化該組件當前狀態的state值必須是一個javascript對象,不能是string,或者number,boolean等簡單的基本數據類型
即使你想要存儲一個只是數字類型的數據,也只能把它存作state對象下的某個字段對應的值中,這個state可以看做是組件自身提供的一個固定的對象,用于存儲當前組件自身的狀態,它是私有的對象,并且完全只受控于當前組件
在以上代碼中,通過給button按鈕監聽綁定onClick屬性掛載點擊事件處理函數(上面是handleBtnClick),來達到控制組件state中的isShow這個狀態,從而讓文本顯示還是隱藏
顯示和隱藏是通過添加class層疊樣式進行設置,但是控制這個行為切換動作的,卻是js
這里用的是箭頭函數,如果不用此方法,一定要記得用bind進行this壞境的綁定
在代碼中,通過this.state可以讀取當前組件狀態的state,但是想要改變state的狀態,并不是直接通過this.state進行更改,而是通過React內置提供的一個setState方法進行觸發的
為了解釋不能直接更改this.state,我們來看另一個加減數字的例子,代碼如下所示
import React, { Fragment, Component } from 'react';
import ReactDOM from 'react-dom';
import "./index.css";
class Count extends Component {
constructor(props){
super(props);
this.state = {
count: 0
}
// this壞境的綁定
this.handleBtnIncrease = this.handleBtnIncrease.bind(this);
this.handleBtnReduce = this.handleBtnReduce.bind(this);
}
render(){
return (
<Fragment>
<div style = {{textAlign: "center"}}>
<button onClick = { this.handleBtnReduce }>-</button>
<span className = "text">{ this.state.count}</span>
<button onClick = { this.handleBtnIncrease }>+</button>
</div>
</Fragment>
);
}
handleBtnReduce() {
this.setState({
count: this.state.count-1
});
}
handleBtnIncrease() {
// 嘗試直接更改this.state的值,這樣是有問題的
this.state.count = this.state.count+1;
}
}
const container = document.getElementById('root');
ReactDOM.render(<Count />, container);
當你點擊加按鈕的時候,頁面不會有任何反應,打開控制臺,會有一個警告提示 不要直接的更改state的值,當你在點擊減號時,你會發現計數發生階躍性變化,比如初始計數值是0的情況下,在你連續點擊加按鈕三次時,計數值沒有發生任何變化
但是當你點擊減號時計數值就會變成2,這個就非常詭異了,效果如下所示
直接修改this.state的值,雖然改變了組件的內部狀態,但是并沒有驅動組件進行重新渲染,既然組件沒有重新渲染,頁面上的UI這個this.state當然不會有任何變化
但是React中的setState方法卻能夠觸發頁面的渲染,它可以接收一個對象或者函數
正確的寫法應當是:利用setState進行對組件state的更改
handleBtnIncrease() {
this.setState({
count: this.state.count+1;
});
}
React中setState要知道的
定義: setState方法是React中React.Component組件所提供的一個內置的方法,當你調用這個setState方法的時候,React會更新組件的狀態state,并且重新調用render方法,最終實現當前組件內部state的更新,從而最新的內容也會渲染到頁面上
作用:修改組件的內部state的狀態,往往用于更新用戶界面以響應事件處理器和處理服務器數據的主要方式
參數:setState函數接收參數有兩種方式,一個是對象,另一個是函數
注意事項
不能直接修改state,它并不會重新渲染組件,如下所示
// 錯誤的寫法 this.state.xxx = "新的值"
this.state.count = this.state.count+1;
應該使用setState()函數去更新當前組件的狀態
<!--this.setState({-->
<!-- xxx: "新的值" -->
<!--});-->
this.setState({
count: this.state.count+1
})
一般而言,通過在React中封裝的事件,例如:onChange,onClick,onKeyDown,onFocus,onBlur等這些事件類型里面綁定事件方法內的setState都是異步的
有時候,this.props和this.state可能會異步更新,在調用setState之后,并不會立馬更新組件
其實它是會批量延遲更新
也就是props,state的值并不會立馬的映射更新,它是把這個state對象放到一個更新隊列里面,然后從隊列當中把新的狀態提出來合并到state中,最后在觸發render函數組件的更新,從而導致UI界面的改變
你不能依賴它來更新下一個狀態
對于SetState什么時候同步什么時候異步?如果是React控制的事件處理程序以及在它的鉤子(生命周期)函數內調用setState,它不會同步的更新state
也就是說:React控制之外的事件調用setState是同步更新的,例如原生js綁定的事件,setTimeout/setInterval等,當然在React中絕大多數都是異步處理的
對于實現同步,我們可以看一下下面這個代碼,先看下效果:點擊減號(-)按鈕,頁面上count變化與控制臺上的值的對應關系,點擊加(+)按鈕與另加按鈕,觀看控制臺也頁面UI效果
import React, { Fragment, Component } from 'react';
import ReactDOM from 'react-dom';
import "./index.css";
class Count extends Component {
constructor(props){
super(props);
this.state = {
count: 10
}
// this壞境的綁定
this.handleBtnIncrease = this.handleBtnIncrease.bind(this);
this.handleBtnReduce = this.handleBtnReduce.bind(this);
}
render(){
return (
<Fragment>
<div style = {{textAlign: "center"}}>
<button onClick = { this.handleBtnReduce }>-</button>
<span className = "text">{ this.state.count}</span>
<button onClick = { this.handleBtnIncrease }>+</button>
<button id="btn-add">另加</button>
</div>
</Fragment>
);
}
// 通過React綁定監聽的onClick事件類型綁定的方法內的setState方法都是異步的
handleBtnReduce() {
this.setState({
count: this.state.count-1
});
console.log("點擊減-count值",this.state.count);
}
handleBtnIncrease() {
setTimeout(() => {
this.setState({
count: this.state.count+1
});
console.log("點擊加-count的值", this.state.count);
},10);
}
// 非React綁定的事件類型方法內調用的setState,是同步的
componentDidMount() {
const btnAdd = document.getElementById('btn-add');
btnAdd.addEventListener('click', () => {
this.setState({
count: this.state.count+1
});
console.log(this.state.count);
});
}
}
const container = document.getElementById('root');
ReactDOM.render(<Count />, container);
以上通過setTimeout/setInterval等addEventListener,以js的事件綁定方式內調用setState方法,此時,state的值將是同步更新的
如果要追究setState內部執行過程,其實它是很復雜的,包括了更新state,以及各個生命周期函數,以后有時間單獨在詳聊的
在這里,你只需要只知道,對于在React中的JSX綁定的事件處理函數中調用setState方法是異步的就可以了
如果你需要基于當前的state來計算出新的值,那么setState函數就應該傳遞一個函數,而不是一個對象,它可以確保每次調用的都是使用最新的state,這一點正是取決于是否傳對象和函數的區別
多個setState調用會合并處理
當在事件處理方法內多次調用setState方法時,render函數只會執行一次,并不會導致組件的重復渲染,因為React會將多個this.setState產生的修改放在一個隊列里面進行批量延遲處理,所以從這點上講,React設計這個setState函數是非常高效的,結合了函數式編程,不用考慮性能的問題
如下代碼所示: 在事件處理程序內調用setState方法改變state的值,雖然是兩次調用但是并不會引起render函數的重復渲染,它會合并成到一個隊列中執行一次操作,只有state或者props發生改變時,它才會引起render函數的重新渲染
import React, { Fragment, Component } from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
import "./index.css";
class ChangeText extends Component {
constructor(props){
super(props);
this.state = {
desc: "歡迎關注微信itclanCoder公眾號",
isStatus: true
}
// this壞境的綁定
this.handleChangeText = this.handleChangeText.bind(this);
}
render(){
console.log("render變化了");
const name = this.state.isStatus? this.props.name:"隨筆川跡";
const age = this.state.isStatus? this.props.age: 20;
return (
<Fragment>
<div style = {{textAlign: "center"}}>
<div>{ this.state.desc }</div>
<div>{ name },永遠的{ age }歲</div>
<button onClick = { this.handleChangeText }>點擊按鈕改變上方文字</button>
</div>
</Fragment>
);
}
handleChangeText() {
this.setState((prevState, newProps) => ({
isStatus: !prevState.isStatus
}));
this.setState({
desc: "學習React",
});
}
}
ChangeText.defaultProps = {
name: "川川",
age: 25
}
ChangeText.propTypes = {
name: PropTypes.string,
age: PropTypes.number
}
const container = document.getElementById('root');
ReactDOM.render(<ChangeText name="川川,一個帥小伙" age={ 18 } />, container);
刷新瀏覽器,查看render函數執行的次數,當點擊按鈕時,只要state和props發生了改變,render函數就會重新渲染
從上面的代碼中,在事件處理函數中調用setState方法時,當setState函數傳遞的是一個函數時,這個函數接收兩個形參數,第一個參數prevState(參數名任意),是先前組件狀態時的state,而后一個參數newProps(形參名任意)是此次更新被應用時的props,它不是必傳的,具體視情況而定
直到現在,知道給setState函數傳遞一個對象與傳遞一個函數的區別是什么?
傳遞一個函數可以讓你在函數內訪問到當前的state的值,因為setState的調用是異步的,this.state.以及this.props不會立即更新,它會被放置到一個隊列中延遲合并處理
只有當state和props數據發生改變時,render函數才會重新渲染
所以你是可以鏈式的進行更新,并確保它們是建立在另一個之上的,這樣不會發生沖突
這也正是setState函數傳遞一個函數的原因,絕大多數時候,最優的方式是,你傳遞一個函數給setState就可以了,并給該函數傳遞兩個形參,然后通過當中的形參來更新state就可以避免詭異的bug了
小結一下:
setState函數是用于更新當前組件的狀態的,不僅可以更改props也可以更改state
它接收兩種參數形式,一個是對象,另一個是函數
當需要基于當前的state計算出新的值進行處理,給setState函數應該傳遞一個函數而不是對象,這樣可以保證每次調用的狀態值都是最新的
至于為什么React不選擇同步更新this.state
這是因為React是有意這么設計的,做異步等待,在constructor構造器函數執行完后,在執行render函數,直到所有組件的事件處理函數內調用setState函數完成之后,避免不必要的重新渲染來提升性能
你可以能會想,React不能對this.state進行立馬更新,而不對組件進行重新渲染呢
如果this.state能立即更新改變,就會破壞組件的協調,只有當props或者state發生改變時,React通過將最新返回的JSX元素與原先的元素進行對比(diff算法),來決定是否有必要進行一次DOM節點的更新,如果前后JSX元素不相等,那么React才會更新DOM
如果props或者state能被直接被修改,將會破壞組件復用的原則,會出現一些莫名其妙的bug
如何劃分組件的狀態數據
無論是props還是state都是組件的數據,影響組件最終的UI展示,究竟怎么樣進行區分,哪個組件應該擁有某個state狀態,進行設置,有時候,它們是非常模糊的概念
但是在React中應該遵循一些原則:
讓組件盡可能的少狀態
如果該組件只是用于UI渲染,數組展示,并無復雜的頁面邏輯交互,那么應該讓組件的數據定義成props,通過外部組件傳入,而并非將數據設置到狀態當中去
那么究竟什么樣的數據屬性可以視為狀態?
狀態(state)應該是會隨著時間產生變化的數據,當更改這個狀態(state),需要更新組件的UI,就可以將它定義成state,更多是在實現頁面的交互時使用的
另一種程度上講,在寫靜態,沒有任何交互頁面時,不應該用state定義當前組件的狀態用來填充頁面
而應該能用外部世界(組件)傳來的數據,就用外部組件傳來的props進行數據的填充
下面的這些就不是狀態(state),不應該定義成state,如何判定該用props還是state,可以進行自我的”靈魂拷問“
該數據是否由父組件(外部世界)通過props傳遞給子組件而來的?如果是,那么它就不是state
通過state或者props可以計算出的數據:比如一個數組的長度等,那么它就不是state
它是否隨著時間的變化而保持不變?如果不改變,那么它也不應該是state:例如:某些頁面固定的標題,字段
與props重復的數據,除非這個數據后期是需要做變更的
而針對這種無狀態的組件(UI組件/函數式組件)
可以用純粹的函數來定義,所謂純函數,只有輸入和輸出,無狀態,無生命周期鉤子函數,只是用作于接收父組件傳來的props值渲染生成DOM結構,無交互,無邏輯層的數據展示
無狀態(函數式)組件,在性能上是最高效的,開銷很低,因為沒有那些生命周期函數嘛
就是一普普通通的函數,執行效率是很高的
UI = render(data)
還記得上次提過上面的公式?React組件扮演的角色應該就是一個純函數(UI組件),它是沒有任何副作用的,由于組件的復用性原則,是不能直接修改props的值的
如果該組件只用于做數據層展示,無需添加生命周期函數等,就可以毫無懸念的使用無狀態組件去定義,當然用箭頭函數也是可以的,它就是普通函數一簡寫的替換,但是要注意,箭頭函數沒有this的綁定
const Header = (props) => {
return (
<div>Hello, {props.content}</div>
);
}
const container = document.getElementById('root')
ReactDOM.render(<Header content="itclanCoder">, container)
props與state的靈魂對比
共同點:
都是組件內的數據,是一普通的javascript對象,都是用來保存信息的,這些信息可以控制組件的形態
不同點:
props是由父組件傳入的(類似形參),用于定義外部組件的接口,是React組件的輸入,它是從父組件傳遞給子組件的數據對象,在父(外部)組件JSX元素上,以自定義屬性的形式定義,傳遞給當前組件,而在子組件內部,則以this.props或者props進行獲取
props只具備讀的能力,不能直接被修改,如果想要修改某些值,用來響應用戶的輸入或者輸出響應,可以借用React內提供的setState函數進行觸發,并用state來作為替代
state是當前組件的內部狀態,它的作用范圍只局限于當前組件,它是當前組件的一個私有變量.用于記錄組件內部狀態的,如果組件中的一些數據在某些時刻發生變化,或者做一些頁面邏輯交互時,需要更新UI,這個時候就需要使用state來跟蹤狀態(例如控制一元素的顯示隱藏來回切換等狀態),它由組件本身管理,可以通過setState函數修改state
總結
本文主要講述了React組件中的數據屬性-state,它是組件內部的狀態,是一私有的變量,用于記錄組件內部狀態,由于props不可修改,通過React中內置提供setState方法修改state的值,并且定義state時,它只能是一個對象,用于存儲組件內部的特殊的狀態
并且大篇幅的講到setState這個函數需要知道的,可接收兩種類型的參數,一個是對象,另一個是函數,以及這兩種方式的區別,如何劃分組件的狀態數據,原則上是盡可能的減少組件的狀態。以及最后的props與state的靈魂對比
雖然可以簡單的用幾句話概括props與state的作用,但是它們是非常重要的,往往程序的bug,就是通過props和state進行追蹤查案的線索,是否經得起自己靈魂的拷問,我覺得至今我也在摸索..
能夠以props和state這種形式順藤摸瓜,尋本溯源到頁面上任何一個UI組件,這種React的能力可以說非常重要了
長路漫漫,其修遠兮,待到山花爛漫時,她在叢中笑-共勉