React 中 setState 使用注意事項

1. 不能直接設置this.state

這個基本學習過 react 的讀者都不會犯這樣的錯,直接設置 this.state 的值並不能觸發組件 render(),正確的是調用 setState() 函數來處理。

2. setState() 回調函數

我們在用 setState() 時,疑惑比較多的地方就是 setState() 可能不會立即生效。基於這一點,慢慢地我們就形成了 setState() 總是異步執行的假象。其實官方文檔裏也說的是可能不會立即生效,後面會講到立即生效的場景。其實 setState() 會批量推遲更新。這使得在調用 setState() 後立即讀取 this.state 成爲了隱患。例如以下示例:

hanldClick = () => {
   // 初始counter=0
   this.setState({
      counter: this.state.counter + 1
   });
   console.log(this.state.counter); // 0
};

setState() 之後直接調用取 counter 的值的話,其實取到的還是生效之前的值,如果我們想要在 counter 生效之後取值的話,可以使用 componentDidUpdate 或者 setState 的回調函數(setState(updater, callback)),這兩種方式都可以保證在應用更新後觸發。我們常用的方式就是 setState 的第二個入參(回調函數)。上述的示例就可以修改成如下:

hanldClick = () => {
   // 初始counter=0
   this.setState({
      counter: this.state.counter + 1
   },()=>{
      console.log(this.state.counter); // 1
   });
};

如果在 render 中也加上打印信息,可以發現,上述的打印信息會在 render 之後打印。

3. setState() 批量執行

我們將上面的示例變更一下:

hanldClick = () => {
   this.setState(
      {
         counter: this.state.counter + 1
      },
      () => {
         console.log(this.state.counter);
      }
   );
   this.setState(
      {
         counter2: this.state.counter2 + 2
      },
      () => {
         console.log(this.state.counter2);
      }
   );
};

在 render 裏也加上打印信息,我們可以發現上述操作並不會出發兩次 render ,而是將兩次 setState() 合併在一起執行了,與下面是等效的:

hanldClick = () => {
   this.setState(
      {
         counter: this.state.counter + 1,
         counter2: this.state.counter2 + 2
      },
      () => {
         console.log(this.state.counter);
         console.log(this.state.counter2);
      }
   );
};

再看下面一個示例:

hanldClick = () => {
   // 初始counter=0
   this.setState({
     counter: this.state.counter + 1
   });
   this.setState({
     counter: this.state.counter + 1
   });
   this.setState({
     counter: this.state.counter + 1
   });
 };

上述操作生效後,counter 的值爲 1,而不是 3。這是因爲 setState() 不一定是立即生效的,而且是批量執行,所以上述的 三個 setState() 中 拿到的 this.state.counter 值都是 0,所以合併起來就等效與:

hanldClick = () => {
   // 初始counter=0
   this.setState({
     counter: this.state.counter + 1
   });
 };

如果要實現上述的操作,可以採用以下 setState() 第一個入參爲函數的方法。

4. setState() 第一個入參爲函數

從[官方文檔(https://zh-hans.reactjs.org/docs/react-component.html#setstate)中可知,setState() 第一個參數除了可以傳一個對象(nextState,nextState會與當前state做淺 merge 操作),還可以傳函數,該函數中接收的 state 和 props 都保證爲最新,且返回值會與 state 進行淺 merge 操作。:

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

因此要實現上述的場景,就可以改寫成如下:

hanldClick = () => {
   // 初始counter=0
   this.setState((state,props)=>({
      counter: state.counter + 1
   }),()=>{
      console.log(this.state.counter); // 3
   });
   this.setState((state, props) => ({
      counter: state.counter + 1
   }),()=>{
      console.log(this.state.counter); // 3
   });
   this.setState((state, props) => ({
      counter: state.counter + 1
   }),()=>{
      console.log(this.state.counter); // 3
   });
   console.log(this.state.counter); // 0
};

5. 原生事件中修改狀態

先看一個示例:

class Index extends Component {
   constructor(props) {
      super();
      this.state = {
         counter: 0,
         counter2: 0
      };
   }
   componentDidMount() {
      let elem = document.getElementById("btn");
      elem.addEventListener("click", this.changeValue, false);
   }

   changeValue = () => {    
      this.setState({
            counter: this.state.counter+1
      });
      console.log(this.state.counter) 
   }

   hanldClick = () => {
      this.setState({
         counter: this.state.counter + 1
      });
      console.log(this.state.counter) 
   };

   render() {
      console.log("======== indexPage render ========");
      console.log(this.state);
      return (
         <div>
         <p>counter: {this.state.counter}</p>
         <button onClick={this.hanldClick}>增加</button>
         <button id="btn">原生事件</button>
         </div>
      );
   }
}

通過原生事件方式添加的點擊事件中 changeValue 中的打印信息可以直接打印出變更之後的 counter, 而通過react的 onClick 事件中的打印信息卻還是變更之前的值。

在 React 的 setState 函數實現中,會根據一個變量 isBatchingUpdates 判斷是直接更新 this.state 還是放到隊列中回頭再說,而 isBatchingUpdates 默認是 false,也就表示 setState 會同步更新 this.state,但是,有一個函數 batchedUpdates,這個函數會把 isBatchingUpdates 修改爲 true,而當 React 在調用事件處理函數之前就會調用這個 batchedUpdates,造成的後果,就是由 React 控制的事件處理過程 setState 不會同步更新 this.state。

  • 也就是說上述的 onClick 是 React 控制的事件處理過程,所以 isBatchingUpdates 修改爲 true,導致不會同步更新this.state;

  • 而添加的原生事件不會修改 isBatchingUpdates 的值,還是默認 false, 所以會同步更新 this.state。

6. setTimeout

我們在上面的 hanldClick 中加一個 setTimeout 邏輯:

hanldClick = () => {
   // 初始counter=0
   this.setState({
      counter: this.state.counter + 1
   });
   console.log(this.state.counter); // 0

   setTimeout(() => {
      this.setState({
         counter: this.state.counter + 1
      });
      console.log(this.state.counter); // 2
      this.setState({
         counter: this.state.counter + 1
      });
      console.log(this.state.counter); // 3
   }, 0);
};

上述第一個 setState() 之後打印 0 的原因上面已經提到過了。至於後面 setTimeout 中的打印結果,可能會有一些疑惑。其實還是 isBatchingUpdates 值的原因,因爲 setTimeout 裏面的回調函數已經不是是 React 控制的事件處理過程了, isBatchingUpdates 的值還是默認的 false, 所以會同步更新 this.state。


reference:
1、https://zh-hans.reactjs.org/docs/react-component.html#setstate

2、https://www.zhihu.com/question/66749082/answer/246217812

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