React學習:狀態(State) 和 屬性(Props)

React :元素構成組件,組件又構成應用。
React核心思想是組件化,其中 組件 通過屬性(props) 和 狀態(state)傳遞數據。


State 與 Props 區別

props 是組件對外的接口,state 是組件對內的接口。組件內可以引用其他組件,組件之間的引用形成了一個樹狀結構(組件樹),如果下層組件需要使用上層組件的數據或方法,上層組件就可以通過下層組件的props屬性進行傳遞,因此props是組件對外的接口。組件除了使用上層組件傳遞的數據外,自身也可能需要維護管理數據,這就是組件對內的接口state。根據對外接口props 和對內接口state,組件計算出對應界面的UI。

主要區別:

  • State是可變的,是一組用於反映組件UI變化的狀態集合;
  • 而Props對於使用它的組件來說,是隻讀的,要想修改Props,只能通過該組件的父組件修改。
    在組件狀態上移的場景中,父組件正是通過子組件的Props, 傳遞給子組件其所需要的狀態。

Props的使用

當一個組件被注入一些屬性(Props )值時,屬性值來源於它的父級元素,所以人們常說,屬性在 React 中是單向流動的:從父級到子元素。

1、props(屬性) 默認爲 “true”

如果你沒給 prop(屬性) 傳值,那麼他默認爲 true 。下面兩個 JSX 表達式是等價的:

<MyTextBox autocomplete />
<MyTextBox autocomplete={true} />

通常情況下,我們不建議使用這種類型,因爲這會與ES6中的對象shorthand混淆 。ES6 shorthand 中 {foo} 指的是 {foo: foo} 的簡寫,而不是 {foo: true} 。這種行爲只是爲了與 HTML 的行爲相匹配。
(舉個例子,在 HTML 中,< input type=“radio” value=“1” disabled /> 與 < input type=“radio” value=“1” disabled=“true” /> 是等價的。JSX 中的這種行爲就是爲了匹配 HTML 的行爲。)

2、props擴展

如果你已經有一個 object 類型的 props,並且希望在 JSX 中傳入,你可以使用擴展操作符 … 傳入整個 props 對象。這兩個組件是等效的:

function App1() {
  return <Greeting firstName="Ben" lastName="Hector" />;
}

function App2() {
  const props = {firstName: 'Ben', lastName: 'Hector'};
  return <Greeting {...props} />;
}

顯然下面的方法更方便:因爲它將數據進行了包裝,而且還簡化了賦值的書寫


State

一、State是什麼?

React 的核心思想是組件化,而組件中最重要的概念是State(狀態),State是一個組件的UI數據模型,是組件渲染時的數據依據。

狀態(state) 和 屬性(props) 類似,都是一個組件所需要的一些數據集合,但是state是私有的,可以認爲state是組件的“私有屬性(或者是局部屬性)”。

如何判斷是否爲State ?

組件中用到的一個變量是不是應該作爲組件State,可以通過下面的4條依據進行判斷:

  • 這個變量是否是通過Props從父組件中獲取?如果是,那麼它不是一個狀態。
  • 這個變量是否在組件的整個生命週期中都保持不變?如果是,那麼它不是一個狀態。
  • 這個變量是否可以通過其他狀態(State)或者屬性(Props)計算得到?如果是,那麼它不是一個狀態。
  • 這個變量是否在組件的render方法中使用?如果不是,那麼它不是一個狀態。這種情況下,這個變量更適合定義爲組件的一個普通屬性,例如組件中用到的定時器,就應該直接定義爲this.timer,而不是this.state.timer。

並不是組件中用到的所有變量都是組件的狀態!當存在多個組件共同依賴一個狀態時,一般的做法是狀態上移,將這個狀態放到這幾個組件的公共父組件中。


二、如何正確使用 State

1、用setState 修改State

直接修改state,組件並不會重新觸發render()

// 錯誤
this.state.comment = 'Hello';

正確的修改方式是使用setState()

// 正確
this.setState({comment: 'Hello'});

2、State 的更新是異步的

  • 調用setState後,setState會把要修改的狀態放入一個隊列中(因而 組件的state並不會立即改變);
  • 之後React 會優化真正的執行時機,來優化性能,所以優化過程中有可能會將多個 setState 的狀態修改合併爲一次狀態修改,因而state更新可能是異步的。
  • 所以不要依賴當前的State,計算下個State。當真正執行狀態修改時,依賴的this.state並不能保證是最新的State,因爲React會把多次State的修改合併成一次,這時,this.state將還是這幾次State修改前的State。
    另外需要注意的事,同樣不能依賴當前的Props計算下個狀態,因爲Props一般也是從父組件的State中獲取,依然無法確定在組件狀態更新時的值。

綜上所述:
this.props 和 this.state 可能是異步更新的,你不能依賴他們的值計算下一個state(狀態)

例:
這樣 counter(計數器)會更新失敗

// 錯誤
this.setState({
  counter: this.state.counter + this.props.increment,
});

要彌補這個問題,使用 setState() 的另一種形式,它接受一個函數而不是一個對象。這個函數有兩個參數:
(1)第一個參數: 是當前最新狀態的前一個狀態(本次組件狀態修改前的狀態)
(2)第二個參數:是當前最新的屬性props

// 正確
this.setState((prevState, props) => ({
  counter: prevState.counter + props.increment
}));

//注意:下面這樣是錯的
this.setState((prevState, props) => { //沒將{}用()括起來,所以會解析成代碼塊
  counter: prevState.counter + props.increment
});

如果你還不懂沒關係,看下面例子:
我們現在渲染出一個button,想每點擊一下,counter就+3
看下面代碼:

class App extends React.Component {
  state = {
    counter: 0,
  }
  handleClick = () => {
    const { counter } = this.state;
    //或者 const counter = this.state.counter;
    this.setState({ counter: counter + 1 });
    this.setState({ counter: counter + 1 });
    this.setState({ counter: counter + 1 });
  }
  render() {
    return (
      <div>
        counter is: {this.state.counter}
        <button onClick={this.handleClick} >點我</button>
      </div>
    )
  }
}
ReactDOM.render(<App />, document.getElementById('root'));

輸出:
這裏寫圖片描述
每點擊一下,加+1,並不是+3
這裏寫圖片描述

之所以+1,不是+3,是因爲 state 的更新可能是異步的,React 會把傳入多個 setState的多個 Object “batch” 起來合併成一個。合併成一個就相當於把傳入 setState 的多個 Object 進行 shallow merge,像這樣:

const update = {
    counter: counter + 1,
    counter: counter + 1,
    counter: counter + 1
    //因爲上面三句話都一樣,所以會當一句話執行
 }

我們可以這麼做就會成功:看下面

class App extends React.Component {
  state = {
    counter: 0,
  }
  handleClick = () => {
    this.setState(prev => ({ counter: prev.counter + 1 }));
    this.setState(prev => ({ counter: prev.counter + 1 }));
    this.setState(prev => ({ counter: prev.counter + 1 }));
    //這樣是錯的 this.setState(prev => {counter: prev.counter + 1});
    //這樣是錯的 this.setState(prev => {counter:++prev.counter});
    //這樣是錯的 this.setState(prev => {counter:prev.counter++});
  }
  render() {
    return (
      <div>
        counter is: {this.state.counter}
        <button onClick={this.handleClick} >點我</button>
      </div>
    )
  }
}
ReactDOM.render(<App />, document.getElementById('root'));

之所以成功是因爲:傳入多個 setState 的多個 Object 會被 shallow Merge,而傳入多個 setState 的多個 function 會被 "queue" 起來,queue 裏的 function 接收到的 state(上面是 prev )都是前一個 function 操作過的 state。


3、State更新會被合併
官方文檔看不懂不要緊,直接舉個例子你就懂了。

例如一個組件的狀態爲:

this.state = {
  title : 'React',
  content : 'React is an wonderful JS library!'
}

當只需要修改狀態title時,只需要將修改後的title傳給setState:

this.setState({title: 'Reactjs'});

React會合並新的title到原來的組件狀態中,同時保留原有的狀態content,合併後的State爲:

{
  title : 'Reactjs',
  content : 'React is an wonderful JS library!'
}

4、setState裏順序更新

  //history 爲數組
   this.setState({
       history: history.concat([1]),  //(1)
       current: history.length,       //(2)
       nextPlayer: !nextPlayer,       //(3)
  });

執行setState時:先更新history,然後再用更新改變後的history計算current的值,最後再更新nextPlayer


三、根據State類型 更新

當狀態發生變化時,如何創建新的狀態?根據狀態的類型,可以分成三種情況:

1、 狀態的類型是不可變類型(數字,字符串,布爾值,null, undefined)

這種情況最簡單,直接給要修改的狀態賦一個新值即可

//原state
this.state = {
  count: 0,
  title : 'React',
  success:false
}
//改變state
this.setState({
  count: 1,
  title: 'bty',
  success: true
})

2、 狀態的類型是數組
數組是一個引用,React 執行 diff 算法時比較的是兩個引用,而不是引用的對象。所以直接修改原對象,引用值不發生改變的話,React 不會重新渲染。因此,修改狀態的數組或對象時,要返回一個新的數組或對象。
(1)增加
如有一個數組類型的狀態books,當向books中增加一本書(chinese)時,使用數組的concat方法或ES6的數組擴展語法

// 方法一:將state先賦值給另外的變量,然後使用concat創建新數組
let books = this.state.books; 
this.setState({
  books: books.concat(['chinese'])
})

// 方法二:使用preState、concat創建新數組
this.setState(preState => ({
  books: preState.books.concat(['chinese'])
}))

// 方法三:ES6 spread syntax
this.setState(preState => ({
  books: [...preState.books, 'chinese']
}))

(2)截取
當從books中截取部分元素作爲新狀態時,使用數組的slice方法:

// 方法一:將state先賦值給另外的變量,然後使用slice創建新數組
let books = this.state.books; 
this.setState({
  books: books.slice(1,3)
})
// 
// 方法二:使用preState、slice創建新數組
this.setState(preState => ({
  books: preState.books.slice(1,3)
}))

(3)條件過濾
當從books中過濾部分元素後,作爲新狀態時,使用數組的filter方法:

// 方法一:將state先賦值給另外的變量,然後使用filter創建新數組
var books = this.state.books; 
this.setState({
  books: books.filter(item => {
    return item != 'React'; 
  })
})

// 方法二:使用preState、filter創建新數組
this.setState(preState => ({
  books: preState.books.filter(item => {
    return item != 'React'; 
  })
}))

注意:不要使用push、pop、shift、unshift、splice等方法修改數組類型的狀態,因爲這些方法都是在原數組的基礎上修改,而concat、slice、filter會返回一個新的數組。

3、狀態的類型是普通對象(不包含字符串、數組)
對象是一個引用,React 執行 diff 算法時比較的是兩個引用,而不是引用的對象。所以直接修改原對象,引用值不發生改變的話,React 不會重新渲染。因此,修改狀態的數組或對象時,要返回一個新的對象。
使用ES6 的Object.assgin方法

// 方法一:將state先賦值給另外的變量,然後使用Object.assign創建新對象
var owner = this.state.owner;
this.setState({
  owner: Object.assign({}, owner, {name: 'Jason'})
})

// 方法二:使用preState、Object.assign創建新對象
this.setState(preState => ({
  owner: Object.assign({}, preState.owner, {name: 'Jason'})
}))

使用對象擴展語法(object spread properties)

// 方法一:將state先賦值給另外的變量,然後使用對象擴展語法創建新對象
var owner = this.state.owner;
this.setState({
  owner: {...owner, name: 'Jason'}
})

// 方法二:使用preState、對象擴展語法創建新對象
this.setState(preState => ({
  owner: {...preState.owner, name: 'Jason'}
}))


綜上所述:
創建新的狀態對象的關鍵是,避免使用會直接修改原對象的方法,而是使用可以返回一個新對象的方法。


四、State向下流動

我們說 props 是組件對外的接口,state 是組件對內的接口。
一個組件可以選擇將 state(狀態) 向下傳遞,作爲其子組件的 props(屬性):

<MyComponent title={this.state.title}/>

這通常稱爲一個“從上到下”,或者“單向”的數據流。任何 state(狀態) 始終由某個特定組件所有,並且從該 state(狀態) 導出的任何數據 或 UI 只能影響樹中 “下方” 的組件。

如果把組件樹想像爲 props(屬性) 的瀑布,所有組件的 state(狀態) 就如同一個額外的水源匯入主流,且只能隨着主流的方向向下流動。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章