天天看點

React State(狀态): React通過this.state來通路state,通過this.setState()方法來更新stateReact State(狀态)

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馬上生效。

繼續閱讀