setState:這個API設計到底怎麼樣

最近,長發飄飄的Eric Elliott發推噴setState,但是他的觀點招來很多反對的聲音,甚至是React團隊成員的反對聲音,於是Eric在Medium上發文繼續噴setState,詳細內容可以看這裡setState() Gate。

說一個東西是好是壞,每個人可以有自己的觀點,咱們也不用繼續爭論,但是Eric在文中有一句話我覺得應該毫無爭議的。

If people frequently get confused about an API, it could be an opportunity to improve that API, or at least improve the documentation.

翻譯過來就是這句話。

如果一個API要是總把人搞糊塗,那就應該改進這個API,至少要應該改進文檔。

誰也不想用一個不爽的API,對不對?

今天我也來說說setState這個API到底怎麼樣,是不是合理,是不是有可以改進之處。

React抽象來說,就是一個公式

UI=f(state)

我們把最終繪製出來的UI當做一個函數f運行的結果,f就是React和我們基於React寫得代碼,而f的輸入參數就是state

作為React管理state的一個重要方法,setState肯定非常重要,如果只是簡單用法,也不會有任何問題,但是如果用得深,就會發現很……尷尬。

我剛開始接觸React的時候,就意識到React相當於一個jQuery的替代品,但是就像單獨依靠jQuery難以管理大型項目,所以也需要給配合使用的MVC框架找一個替代品,我選擇的替代品是Redux,我很早就將React和Redux配合使用;現在,回過頭來看看React的setState,發現坑真的不少,不禁感嘆自己還是挺走運的。

對setState用得深了,就容易犯錯,所以我們開門見山先把理解setState的關鍵點列出來。

  1. setState不會立刻改變React組件中state的值;
  2. setState通過引發一次組件的更新過程來引發重新繪製;
  3. 多次setState函數調用產生的效果會合併。

這幾個關鍵點其實是相互關聯的,一個一個說吧。

setState不會立刻改變React組件中state的值

在React中,一個組件中要讀取當前狀態用是訪問this.state,但是更新狀態卻是用this.setState,不是直接在this.state上修改,為什麼呢?

//讀取狀態const count = this.state.count//更新狀態this.setState({count: count + 1});//無意義this.state.count = count + 1;

因為this.state說到底只是一個對象,單純去修改一個對象的值是沒有意義的,去驅動UI的更新才是有意義的,想想看,如果只是改了this.state這個對象,但是沒有讓React組件重新繪製一遍,那有什麼用?你可以嘗試在代碼中直接修改this.state的值,會發現的確能夠改變狀態,但是卻不會引發重新渲染。

所以,需要用一個函數去更改狀態,這個函數就是setState,當setState被調用時,能驅動組件的更新過程,引發componentDidUpdate、render等一系列函數的調用。

當然,如果使用Object的setter功能,實際上也可以通過對this.state對象的直接修改來實現setState一樣的功能,但是,如果React真的這麼設計的話,我敢肯定,那樣的API設計會更讓人暈頭轉向,因為不管是誰,第一眼也看不出來修改一個this.state對象居然會引發重新渲染的副作用。

這麼看來,React提供setState這個API是一個挺合理的決定。

因為setState並不會立刻修改this.state的值,所以下面的code可能產生很不直觀的結果。

function incrementMultiple() { this.setState({count: this.state.count + 1}); this.setState({count: this.state.count + 1}); this.setState({count: this.state.count + 1});}

直觀上來看,當上面的incrementMultiple函數被調用時,組件狀態的count值被增加了3次,每次增加1,那最後count被增加了3,但是,實際上的結果只給state增加了1。

原因並不複雜,就是因為調用this.setState時,並沒有立即更改this.state,所以this.setState只是在反覆設置同一個值而已,上面的code等同下面這樣。

function incrementMultiple() { const currentCount = this.state.count; this.setState({count: currentCount + 1}); this.setState({count: currentCount + 1}); this.setState({count: currentCount + 1});}

currentCount就是一個快照結果,重複地給count設置同一個值,不要說重複3次,哪怕重複一萬次,得到的結果也只是增加1而已。

既然this.setState不會立即修改this.state的值,那在什麼時候修改this.state的值呢?這就要說一下React的更新生命周期。

setState通過引發一次組件的更新過程來引發重新繪製

setState調用引起的React的更新生命周期函數4個函數(比修改prop引發的生命周期少一個componentWillReceiveProps函數),這4個函數依次被調用。

  • shouldComponentUpdate
  • componentWillUpdate
  • render
  • componentDidUpdate

當shouldComponentUpdate函數被調用的時候,this.state沒有得到更新。

當componentWillUpdate函數被調用的時候,this.state依然沒有得到更新。

直到render函數被調用的時候,this.state才得到更新。

(或者,當shouldComponentUpdate函數返回false,這時候更新過程就被中斷了,render函數也不會被調用了,這時候React不會放棄掉對this.state的更新的,所以雖然不調用render,依然會更新this.state。)

如果你沒興趣去記住React的生命周期(雖然你應該記住),那就可以簡單認為,直到下一次render函數調用時(或者下一次shouldComponentUpdate返回false時)才得到更新的this.state。

不管你喜歡不喜歡,反正this.state就是不會再this.setState調用之後立刻更新。

多次setState函數調用產生的效果會合併

比如下面的代碼。

function updateName() { this.setState({FirstName: "Morgan"}); this.setState({LastName: "Cheng"});}

連續調用了兩次this.setState,但是只會引發一次更新生命周期,不是兩次,因為React會將多個this.setState產生的修改放在一個隊列里,緩一緩,攢在一起,覺得差不多了再引發一次更新過程。

在每次更新過程中,會把積攢的setState結果合併,做一個merge的動作,所以上面的代碼相當於這樣。

function updateName() { this.setState({FirstName: "Morgan", LastName: "Cheng"});}

如果每一個this.setState都引發一個更新過程的話,那就太浪費了!

對於開發者而言,也可以放心多次調用this.setState,每一次只要關注當前修改的那一個欄位就行,反正其他欄位會合併保留,丟不掉。

所以,合併多次this.setState調用更改的狀態這個API設計決定也不錯。

總結一下,setState最招罵的就是不會立即修改this.state。

如果有可能的話,怎麼改進這個API呢?

首先setState肯定還是不能立刻更新this.state,不然React整個概念就被推翻了,所以能做的只是用一個更清楚的方式表達,讓開發者不會誤以為this.state會被this.setState立即更新,好像也沒有特別好的改進方法。

不過,最近一個this.setState函數的隱藏功能進入了大家的視野,那就是:原來this.setState可以接受一個函數作為參數啊!

這真的是React遊戲世界中的一個大彩蛋。

函數式的setState用法

如果傳遞給this.setState的參數不是一個對象而是一個函數,那遊戲規則就變了。

這個函數會接收到兩個參數,第一個是當前的state值,第二個是當前的props,這個函數應該返回一個對象,這個對象代表想要對this.state的更改,換句話說,之前你想給this.setState傳遞什麼對象參數,在這種函數里就返回什麼對象,不過,計算這個對象的方法有些改變,不再依賴於this.state,而是依賴於輸入參數state。

比如,對於上面增加state上count的例子,可以這麼寫一個函數。

function increment(state, props) { return {count: state.count + 1};}

可以看到,同樣是把狀態中的count加1,但是狀態的來源不是this.state,而是輸入參數state。

對應incrementMultiple的函數就是這麼寫。

function incrementMultiple() { this.setState(increment); this.setState(increment); this.setState(increment);}

對於多次調用函數式setState的情況,React會保證調用每次increment時,state都已經合併了之前的狀態修改結果。

簡單說,加入當前this.state.count的值是0,第一次調用this.setState(increment),傳給increment的state參數是0,第二調用時,state參數是1,第三次調用是,參數是2,最終incrementMultiple的效果,真的就是讓this.state.count變成了3,這個函數incrementMultiple終於實至名歸。

值得一提的是,在increment函數被調用時,this.state並沒有被改變,依然,要等到render函數被重新執行時(或者shouldComponentUpdate函數返回false之後)才被改變。

讓setState接受一個函數的API設計很棒!因為這符合函數式編程的思想,讓開發者寫出沒有副作用的函數,我們的increment函數並不去修改組件狀態,只是把「希望的狀態改變」返回給React,維護狀態這些苦力活完全交給React去做。

正因為流程的控制權交給了React,所以React才能協調多個setState調用的關係。

讓我們再往前推進一步,試著如果把兩種setState的用法混用,那會有什麼效果?

我們把incrementMultiple改成這樣。

function incrementMultiple() { this.setState(increment); this.setState(increment); this.setState({count: this.state.count + 1}); this.setState(increment);}

在幾個函數式setState調用中插入一個傳統式setState調用(嗯,我們姑且這麼稱呼以前的setState使用方式),最後得到的結果是讓this.state.count增加了2,而不是增加4。

原因也很簡單,因為React會依次合併所有setState產生的效果,雖然前兩個函數式setState調用產生的效果是count加2,但是半路殺出一個傳統式setState調用,一下子強行把積攢的效果清空,用count加1取代。

這麼看來,傳統式setState的存在,會把函數式setState拖下水啊!只要有一個傳統式的setState調用,就把其他函數式setState調用給害了。

如果說setState這兒API將來如何改進,也許就該完全採用函數為參數的調用方法,廢止對象為參數的調用方法。

當然,React近期肯定不會有這樣的驚世駭俗的改變,但是大家可以先嘗試函數式setState用法,這才是setState的未來。

【更新】後續討論 setState為什麼不會同步更新組件狀態 - 知乎專欄

歡迎關注這個專欄 進擊的React - 知乎專欄


推薦閱讀:

從零學習前端開發·CSS
1.1 React 介紹
React Render Array 性能大亂斗
從輸入 URL 到頁面載入完成的過程中都發生了什麼

TAG:React | 前端开发 | API |