基於React 原始碼深入淺出setState:官方文件的啟示錄

墨成發表於2019-03-03

作者 : 墨成

React
版本 :16.4.1

仔細閱讀官網setState的描述會發現裡面透露的資訊量巨大,我也建議初學者在學習
React之前仔細閱讀原始文件,以下是我個人在閱讀文件時的一些領悟,配合了一些翻譯和講解,限於個人水平,不足之處,各位請多多指出

setState(updater[, callback])複製程式碼

setState() enqueues
changes to the component state and tells React that this component and its
children need to be re-rendered with the updated state. This is the primary
method you use to update the user interface in response to event handlers and
server responses.

翻譯: setState()通過佇列的形式儲存元件狀態並告訴React這個元件和他的子元件需要重新渲染。這是我們通過事件或伺服器響應更新使用者介面的主要方法(也就是說我們最常用)

解釋:無

Think
of setState() as a
request rather than an immediate command to update the component. For better
perceived performance, React may delay it, and then update several components
in a single pass. React does not guarantee that the state changes are applied
immediately.

翻譯: setState()只是作為一個請求而不是一個立刻執行的指令去更新元件。為了更好的效能,React會延遲執行,然後通過一種單一(這個單一在這裡的意思是歸類的意思)的方式去更新幾個元件。React不會立刻把state的改變應用到元件上

解釋:這句的話的意思很明顯就是告訴你 : React的 setState是”非同步”的,React 在沒有重新渲染之前對state的做了一些處理以達到最佳的效能,例項程式碼:

//Async.js
state = {value:`default value`};

changeValue=()=>{
    console.log(`Before change,value is ${this.state.value}`);
    this.setState(
        {value:`I have a new  value`}
    )
    // 通過setState修改了state中value的值,列印的結果並不是最新的值,即修改沒有生效
    console.log(`After change,value is ${this.state.value}`);
    console.log(`Casue setState is asynchronous ,so you will see the same value in this function`);
};複製程式碼
//the result
Before change,value is default value
After change,value is default value
Casue setState is asynchronous ,so you will see the same value in this function
複製程式碼

setState() does not
always immediately update the component. It may batch or defer the update until
later. This makes reading this.state right
after calling setState() a
potential pitfall. Instead, use componentDidUpdate or a setState callback (setState(updater,
callback)
), either of which are guaranteed to fire after the update has
been applied. If you need to set the state based on the previous state, read
about the updater argument
below.

翻譯: setState()並不總是立刻更新元件(言下之意就是有辦法可以立刻更新,後面會講道這部分內容)。隨後它會使用批處理或延遲更新 。在你呼叫setState()後立刻讀取 this.state的值不會生效(原文叫 潛在的陷阱)。相反,使用componentDidUpdate
或者 setState
回撥函式 的任意一種方式都會讓對state的更新生效(原文的作者使用了
fire這個詞非常形象,想象一下這樣的一種場景:你為了一個難題徹夜難眠,絞盡腦汁,突然看到了火(黑暗前的黎明,激動)是希望!!).如果你想基於上一個state來設定state,請閱讀下方updater的引數

解釋:setSate雖然是非同步的,但是我們還是可以通過其他方式讀取到生效的值,比如在react生命週期函式 componentDidUpdatesetState的回撥函式中讀取
。言下之意是告訴我們在 setState完成後,會啟用 componentDidUpdate 周期函式,有回撥函式會執行回撥函式。例項程式碼:

//DidUpdateOrCallback.js
state = {value:`default value`};

changeValue=()=>{
    console.log(`Before change,value is ${this.state.value}`);
    this.setState(
        {value:`I have a new  value`}
    ,()=>{
        console.log(`Look at the value in setState callback function,value is ${this.state.value}`);
    })
};

componentDidUpdate(){
    console.log(`Look at the value in componentDidUpdate(),value is ${this.state.value}`);
}

//result:
Before change,value is default value
Look at the value in componentDidUpdate(),value is I have a new  value
Look at the value in setState callback function,value is I have a new  value
複製程式碼

setState() will
always lead to a re-render unless shouldComponentUpdate() returns false. If mutable
objects are being used and conditional rendering logic cannot be implemented in shouldComponentUpdate(), calling setState() only when
the new state differs from the previous state will avoid unnecessary
re-renders.

翻譯: setState總是會觸發重渲染,除非shouldComponentUpdate() 返回 false .shouldComponentUpdate()不應該包含可變物件作為條件渲染的邏輯,我們僅僅在state發生變化去呼叫setSate而避免不必要的重新渲染

解釋:shouldComponentUpdate()的邏輯中直接比較引用型別是不可以發揮它的作用,就是說對於引用型別,地址總是相同,返回值永遠為true(淺比較).比如:

state = {value:{name:`default name`}};

shouldComponentUpdate(nextProps,nextState){
    //value 是引用型別,比較的記憶體地址,淺比較 ,嘗試直接比較name
    let ret = (this.state.value === nextState.value);
    console.log(`State.value is object ,so ret is always ${ret}.`);
    return !ret;
}

changeValue=()=>{
    let value = this.state.value;
    value.name = `I have a new  name`;
    this.setState({value:value});
};複製程式碼

這裡你會發現 ret 的值永遠為 true,shouldComponentUpdate()總是返回 false,不會觸發re-render.

The
first argument is an updater function with the signature:

(prevState, props) => stateChange複製程式碼

prevState is a
reference to the previous state. It should not be directly mutated. Instead,
changes should be represented by building a new object based on the input from prevStateand props. For instance,
suppose we wanted to increment a value in state by props.step:

翻譯:prevState 是上一個狀態的引用. 它不應該直接被改變。這種變化應該是基於 prevStateprops構建的一個新物件。比如,假如我們想在state中通過props.step去對一個值做增量操作:

解釋:無

this.setState((prevState, props) => {複製程式碼
  return {counter: prevState.counter + props.step};複製程式碼
});複製程式碼

Both prevState and props received
by the updater function are guaranteed to be up-to-date. The output of the
updater is shallowly merged with prevState.

翻譯: updater
function
保證接受到的prevStateprops是最新的(這裡的最新是相對於上次 render後的值,而不是連續呼叫setState的值,這裡可能會讓有些人有點誤解)。呼叫updater function是淺合併(這裡有故事)

解釋:無

The
second parameter to setState() is an
optional callback function that will be executed once setState is
completed and the component is re-rendered. Generally we recommend using componentDidUpdate() for such
logic instead.

翻譯:第二個是可選的回撥函式,它會在setState完成後並且元件重新渲染後立刻執行。一般來說,我們推薦使用componentDidUpdate() 來替換這個邏輯。

解釋: 如果使用回撥函式,React 更建議在componentDidUpdate來處理這個邏輯。就是這個回撥函式在沒有特別必要的情況下不要使用(原始碼對callback做了很多邏輯處理,後面也會提及)

You
may optionally pass an object as the first argument to setState() instead of
a function:

setState(stateChange[, callback])複製程式碼

翻譯:你可以有選擇性的傳遞一個物件作為setState的第一引數

解釋: 除了傳遞函式之外也可以使用物件作為引數

This
performs a shallow merge of stateChange into the
new state, e.g., to adjust a shopping cart item quantity:

this.setState({quantity: 2})複製程式碼

翻譯:它會執行淺合併來生成一個新的state,比如說:修改購物車裡的商品數量

This
form of setState() is also
asynchronous, and multiple calls during the same cycle may be batched together.
For example, if you attempt to increment an item quantity more than once in the
same cycle, that will result in the equivalent of:

Object.assign(複製程式碼
  previousState,複製程式碼
  {quantity: state.quantity + 1},複製程式碼
  {quantity: state.quantity + 1},複製程式碼
  ...複製程式碼
)複製程式碼

翻譯:這種形式的
setState()(把物件作為引數)也是”非同步”的,相同週期(這個cycle的翻譯我還沒找到合適的詞,暫且使用週期,比如同一個函式多次呼叫setState,我們就認為他們在同一個週期)的多次呼叫會被批量執行。比如,如果你試圖在同一週期多次增加商品的數量,那麼它的結果等同於:

解釋:這裡的例子非常關鍵,要理解它必須完全理解Object.assign合併物件的原理,比如說,不同物件相同屬性,後面的物件會覆蓋前面的物件;不同物件不同屬性,會合併到最終的物件上,這裡也寫了一個demo:

state = {numberFunction:0, numberObject: 0};

changeNumberObject=()=>{

    this.setState(
        {numberObject:this.state.numberObject+1}
    );

     this.setState(
        {numberObject:this.state.numberObject+1}
    );

     this.setState(
        {numberObject:this.state.numberObject+1}
     );

      this.setState(
        {numberObject:this.state.numberObject+1}
     );
     //只有最後這個setState才生效 
};

changeNumberFunction=()=>{

    this.setState((preState)=>{
        return  {numberFunction:preState.numberFunction+1}
    })

    this.setState((preState)=>{
        return  {numberFunction:preState.numberFunction+1}
    })

    this.setState((preState)=>{
        return  {numberFunction:preState.numberFunction+1}
    })

    this.setState((preState)=>{
        return  {numberFunction:preState.numberFunction+1}
    })
    //每個都回執行 
};

componentDidUpdate(){
    console.log(`The expected  numberObject is 4,real value is ${this.state.numberObject}`);
    console.log(`The expected  numberFunction is 4,real value is ${this.state.numberFunction}`);
}
複製程式碼

Subsequent
calls will override values from previous calls in the same cycle, so the
quantity will only be incremented once. If the next state depends on the previous
state, we recommend using the updater function form, instead:

this.setState((prevState) => {複製程式碼
  return {quantity: prevState.quantity + 1};複製程式碼
});複製程式碼

翻譯:在同一週期靠後的setState()將會覆蓋前一個setSate()的值(相同屬性名),因此,這個商品數量僅自增了一次,如果希望下一個state依賴上一個state,我們推薦使用函式的形式

解釋:這裡很明顯的告訴我們,setSate第一引數傳遞的物件或函式,react的處理方式不一樣, 這跟React在效能優化有很大關係,為了最小可能性去re-render(重渲染),React原始碼作了很多額外的工作

至此,官方文件對setState()的概要描述已經結束。

系列文章持續更新…..

系列文章一:基於React原始碼深入淺出setState:setState的非同步實現

 

本文所有程式碼,請移步 :github

如果喜歡,不要吝嗇你的 star .

相關文章