1. 程式人生 > >React學習:狀態(State) 和 屬性(Props)

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!'
}

三、根據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(狀態) 就如同一個額外的水源匯入主流,且只能隨著主流的方向向下流動。