ReactState(狀態):React通過this.state來訪問state,通過this.setState()方法來更新state

程式設計師詩人發表於2017-11-23

React State(狀態)

React 把元件看成是一個狀態機(State Machines)。通過與使用者的互動,實現不同狀態,然後渲染 UI,讓使用者介面和資料保持一致。
React 裡,只需更新元件的 state,然後根據新的 state 重新渲染使用者介面(不要操作 DOM)。
以下例項中建立了 LikeButton 元件,getInitialState 方法用於定義初始狀態,也就是一個物件,這個物件可以通過 this.state 屬性讀取。當使用者點選元件,導致狀態變化,this.setState 方法就修改狀態值,每次修改以後,自動呼叫 this.render 方法,再次渲染元件。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>菜鳥教程 React 例項</title>
    <script src="https://cdn.bootcss.com/react/15.4.2/react.min.js"></script>
    <script src="https://cdn.bootcss.com/react/15.4.2/react-dom.min.js"></script>
    <script src="https://cdn.bootcss.com/babel-standalone/6.22.1/babel.min.js"></script>
  </head>
  <body>
    <div id="example"></div>
    <script type="text/babel">
      var LikeButton = React.createClass({
        getInitialState: function() {
          return {liked: false};
        },
        handleClick: function(event) {
          this.setState({liked: !this.state.liked});
        },
        render: function() {
          var text = this.state.liked ? `喜歡` : `不喜歡`;
          return (
            <p onClick={this.handleClick}>
              你<b>{text}</b>我。點我切換狀態。
            </p>
          );
        }
      });

      ReactDOM.render(
        <LikeButton />,
        document.getElementById(`example`)
      );
    </script>
  </body>
</html>

我們都知道,React通過this.state來訪問state,通過this.setState()方法來更新state。當this.setState()方法被呼叫的時候,React會重新呼叫render方法來重新渲染UI

setState非同步更新

setState方法通過一個佇列機制實現state更新,當執行setState的時候,會將需要更新的state合併之後放入狀態佇列,而不會立即更新this.state(可以和瀏覽器的事件佇列類比)。如果我們不使用setState而是使用this.state.key來修改,將不會觸發元件的re-render。如果將this.state賦值給一個新的物件引用,那麼其他不在物件上的state將不會被放入狀態佇列中,當下次呼叫setState並對狀態佇列進行合併時,直接造成了state丟失。(這裡特別感謝@Dcatfly的指正)

我們來看一下React文件中對setState的說明

    void setState(
      function|object nextState,
      [function callback]
    )

The second (optional) parameter is a callback function that will be executed once setState is completed and the component is re-rendered.

翻譯一下,第二個引數是一個回撥函式,在setState的非同步操作結束並且元件已經重新渲染的時候執行。也就是說,我們可以通過這個回撥來拿到更新的state的值。

React也正是利用狀態佇列機制實現了setState的非同步更新,避免頻繁地重複更新state(pending的意思是未定的,即將發生的)

   //將新的state合併到狀態更新佇列中
   var nextState =  this._processPendingState(nextProps, nextContext);
   //根據更新佇列和shouldComponent的狀態來判斷是否需要更新元件
   var shouldUpdate = 
      this._pendingForceUpdate ||
      !inst.shouldComponentUpdate ||
      inst.shouldComponentUpdate(nextProps, nextState, nextContext);

setState迴圈呼叫風險

當呼叫setState時,實際上會執行enqueueSetState方法,並對partialState以及_pending-StateQueue更新佇列進行合併操作,最終通過enqueueUpdate執行state更新

而performUpdateIfNecessary方法會獲取pendingElement, pendingStateQueue,_ pending-ForceUpdate,並呼叫receiveComponent和updateComponent方法進行元件更新

如果在shouldComponentUpdate或者componentWillUpdate方法中呼叫setState,此時this._pending-StateQueue != null,就會造成迴圈呼叫,使得瀏覽器記憶體佔滿後崩潰

呼叫棧

既然setState最終是通過enqueueUpdate執行state更新,那麼enqueueUpdate到底是如何更新state的呢? 首先看下面的問題

   import React, { Component } from `react`;
   class Example extends Component {
       constructor(){
           super();
           //在元件初始化可以直接操作this.state
           this.state = {
               val: 0
           }
       }
       componentDidMount(){
           this.setState({
              val: this.state.val + 1
           });
           //第一次輸出
           console.log(this.state.val);
           this.setState({
              val: this.state.val + 1
           });
           //第二次輸出
           console.log(this.state.val);
           setTimeout(()=>{
              this.setState({val: this.state.val + 1});
               //第三次輸出
               console.log(this.state.val);
               this.setState({
                  val: this.state.val + 1
               });
               //第四次輸出
               console.log(this.state.val);
           }, 0);  
       }
       render(){
           return null;
       }
   }

上述程式碼中,4次console.log列印出來的val分別是: 0,0,2 ,3

我們來看一個簡化的setState的呼叫棧

   this.setState(newState) =>
   newState存入pending佇列 =>
   呼叫enqueueUpdate =>
   是否處於批量更新模式 =>
   是的話將元件儲存到dirtyComponents
   不是的話遍歷dirtyComponents,呼叫updateComponent,更新pending state or props

enqueueUpdate的原始碼如下(上面流程的第三步)(batching的意思是批量的)

   function enqueueUpdate(component){
       //injected注入的
       ensureInjected();
       //如果不處於批量更新模式
       if(!batchingStrategy.isBatchingUpdates){
           batchingStrategy.batchedUpdates(enqueueUpdate, component);
           return;
       }
       //如果處於批量更新模式
       dirtyComponents.push(component);
   }

如果isBatchingUpdates為true,則對所有佇列中的更新執行batchedUpdates方法,否則只把當前元件(即呼叫了setState的元件)放入dirtyComponents陣列中,例子中4次setState呼叫的表現之所以不同,這裡的邏輯判斷起了關鍵作用

事務

事務就是將需要執行的方法使用wrapper封裝起來,再通過事務提供的perform方法執行,先執行wrapper中的initialize方法,執行完perform之後,在執行所有的close方法,一組initialize及close方法稱為一個wrapper。

那麼事務和setState方法的不同表現有什麼關係,首先我們把4次setState簡單歸類,前兩次屬於一類,因為它們在同一呼叫棧中執行,setTimeout中的兩次setState屬於另一類。

在setState呼叫之前,已經處在batchedUpdates執行的事務中了。那麼這次batchedUpdates方法是誰呼叫的呢,原來是ReactMount.js中的_renderNewRootComponent方法。也就是說,整個將React元件渲染到DOM中的過程就是處於一個大的事務中。而在componentDidMount中呼叫setState時,batchingStrategy的isBatchingUpdates已經被設為了true,所以兩次setState的結果沒有立即生效。

再反觀setTimeout中的兩次setState,因為沒有前置的batchedUpdates呼叫,所以導致了新的state馬上生效。


相關文章