解密React state hook

先看個問題,下面組件中若是點擊3次組件Counter的「setCounter」按鈕,控制檯輸出是什麼?html

function Counter() {
    const [counter, setCounter] = useState(1);
    console.log('Counter.render', counter);
    return (
           <>
               <Display counter={counter}/>
               <button onClick={() => setCounter(2)}>setCounter</button>
           </>
   )
}

function Display({ counter }) {
    console.log('Display.render', counter);
    return <p>{counter}</p>
}

.
.
.
正確的答案是:react

  1. 第一次點擊「setCounter」按鈕,state的值變成2觸發一次re-render
    即輸出:git

    Counter.render 2
    Display.render 2
  2. 第二次點擊「setCounter」按鈕,雖然state的值沒有變,但也觸發了一次組件Counter re-render,可是沒有觸發組件Display re-render
    即輸出:github

    Counter.render 2
  3. 第三次點擊「setCounter」按鈕,state沒有變,也沒有觸發re-render

1、更新隊列

1.1 什麼是更新隊列

其實每一個state hook都關聯一個更新隊列。每次調用setState/dispatch函數時,React並不會當即執行state的更新函數,而是把更新函數插入更新隊列裏,並告訴React須要安排一次re-render
舉個栗子:segmentfault

function Counter() {
    const [counter, setCounter] = useState(0);
    console.log('Counter.render', counter);
    return (
           <>
               <Display counter={counter}/>
               <button onClick={() => setCounter(counter + 1)}>Add</button>
               <button onClick={() => {
                   console.log('Click event begin');
                   
                   setCounter(() => {
                       console.log('update 1');
                       return 1;
                   });

                   setCounter(() => {
                        console.log('update 2');
                        return 2;
                   });

                   console.log('Click event end');
               }}>setCounter</button>
           </>
   )
}

先點擊下"Add"按鈕(後面解釋緣由),再點擊「setCounter」按鈕看下輸出:性能優化

Click event begin
Click event end
update 1
update 2
Counter.render 2
Display.render 2

經過例子能夠看出在執行事件處理函數過程當中並無當即執行state更新函數。這主要是爲了性能優化,由於可能存在多處setState/dispatch函數調用。異步

1.2 多個更新隊列

每一個state都對應一個更新隊列,一個組件裏可能會涉及多個更新隊列。函數

  1. 各個更新隊列是互相獨立的;
  2. 各個更新隊列的更新函數執行順序取決於任務隊列建立前後(即調用useState/useReducer的前後順序)。
  3. 同一個更新隊列裏多個更新函數是依次執行的,前一個更新函數的輸出做爲下一個更新函數的輸入(相似管道)。
function Counter() {
    console.log('Counter.render begin');
    const [counter, setCounter] = useState(1);
    const [counter2, setCounter2] = useState(1);
    return (
           <>
               <p>counter1: {counter}</p>
               <p>counter2: {counter2}</p>
               <button onClick={() => {
                    setCounter(() => {
                       console.log('setCounter update1');
                       return 2;
                   })
                    setCounter2(() => {
                        console.log('setCounter2 update1');
                        return 2;
                    })
                    setCounter(() => {
                        console.log('setCounter update2');
                        return 2;
                    })
                    setCounter2(() => {
                        console.log('setCounter2 update2');
                        return 2;
                    })
               }}>setCounter2</button>
           </>
   )
}

點擊"setCounter2"按鈕看看輸出結果。上例中setCounter對應的更新隊列的更新函數永遠要先於setCounter2對應的任務隊列的更新函數執行。性能

2、懶計算

何時執行更新隊列的更新函數呢?懶計算就是執行更新函數的策略之一。懶計算是指只有須要state時React纔會去計算最新的state值,即得等到再次執行useState/useReducer時纔會執行更新隊列裏的更新函數。優化

function Display({ counter }) {
    console.log('Display.render', counter);
    return <p>{counter}</p>
}

function Counter() {
    console.log('Counter.render begin');
    const [counter, setCounter] = useState(0);
    console.log('Counter.render', counter);
    return (
           <>
               <Display counter={counter}/>
               <button onClick={() => setCounter(counter + 1)}>Add</button>
               <button onClick={() => {
                   console.log('Click event begin');
                   
                   setCounter(prev => {
                       console.log(`update 1, prev=${prev}`);
                       return 10;
                   });

                   setCounter(prev => {
                        console.log(`update 2, prev=${prev}`);
                        return 20;
                   });

                   console.log('Click event end');
               }}>setCounter</button>
           </>
   )
}

先點擊下"Add"按鈕,再點擊「setCounter」按鈕看下輸出:

Click event begin
Click event end
Counter.render begin
update 1, prev=1
update 2, prev=10
Counter.render 20
Display.render 20

經過栗子會發現:

  1. 先執行渲染函數,再執行更新函數;
  2. 第二個更新函數的實參就是第一個更新函數的返回值。

3、批處理

在懶計算中只有再次執行渲染函數時纔會知道state是否發生變化。那React何時再次執行組件渲染函數呢?
通常咱們都是在事件處理函數裏調用setState,React在一個批處理裏執行事件處理函數。事件處理函數執行完畢後若是觸發了re-render請求(一次或者屢次),則React就觸發一次且只觸發一次re-render

3.1 特性

1. 一個批處理最多觸發一次re-render, 而且一個批處理裏能夠包含多個更新隊列;

function Counter() {
    console.log('Counter.render begin');
    const [counter1, setCounter1] = useState(0);
    const [counter2, setCounter2] = useState(0);

    return (
           <>
               <p>counter1={counter1}</p>
               <p>counter2={counter2}</p>
               <button onClick={() => {                   
                   setCounter1(10);
                   setCounter1(11);

                   setCounter2(20);
                   setCounter2(21);
               }}>setCounter</button>
           </>
   )
}

點擊"setCounter"按鈕,看下輸出:

Counter.render begin

2. 批處理只能處理回調函數裏的同步代碼,異步代碼會做爲新的批處理;

function Display({ counter }) {
    console.log('Display.render', counter);
    return <p>{counter}</p>
}

function Counter() {
    console.log('Counter.render begin');
    const [counter, setCounter] = useState(0);
    return (
           <>
               <Display counter={counter}/>
               <button onClick={() => {                   
                   setCounter(prev => {
                       return 10;
                   });

                   setTimeout(() => {
                        setCounter(prev => {
                            return 20;
                        });
                   })
               }}>setCounter</button>
           </>
   )
}

點擊"setCounter"按鈕,看下輸出:

Counter.render begin
Display.render 10
Counter.render begin
Display.render 20

觸發兩次批處理。

3. 異步回調函數裏觸發的re-render不會做爲批處理

setTimeout/setInterval等異步處理函數調用並非React觸發調用的,React也就沒法對這些回調函數觸發的re-render進行批處理。

function Display({ counter }) {
    console.log('Display.render', counter);
    return <p>{counter}</p>
}

export default function Counter() {
    console.log('Counter.render begin');
    const [counter, setCounter] = useState(0);
    return (
           <>
               <Display counter={counter}/>
               <button onClick={() => {                   
                    setCounter(prev => {
                       return 10;
                    });

                   setCounter(prev => {
                        return 11;
                    });

                   setTimeout(() => {
                        setCounter(prev => {
                            return 20;
                        });

                        setCounter(prev => {
                            return 21;
                        });
                   })
               }}>setCounter</button>
           </>
   )
}

點擊setCounter按鈕輸出:

Counter.render begin
Display.render 11
Counter.render begin
Display.render 20
Counter.render begin
Display.render 21

能夠看出事件處理函數的裏兩次setState進行了批處理,而setTimeout回調函數裏的兩次setState分別觸發了兩次re-render。

3.2 總結

  1. 能夠觸發批處理的回調函數:
  2. React事件處理函數;
  3. React生命週期函數,如useEffect反作用函數;
  4. 組件渲染函數內部
    在實現getDerivedStateFromProps中會遇到這種調用場景。
  5. 不會觸發批處理的回調函數:
    非React觸發調用的回調函數,好比setTimeout/setInterval等異步處理函數

4、跳過更新

咱們都知道若是state的值沒有發生變化,React是不會從新渲染組件的。可是從上面得知React只有再次執行useState時纔會計算state的值啊。
爲了計算最新的state須要觸發re-render,而state若是不變又不渲染組件,這好像是個先有蛋仍是先有雞的問題。React是採用2個策略跳太重新渲染:

  1. 懶計算
  2. 當即計算

4.1 當即計算

除了上面提到的都是懶計算,其實React還存在當即計算。當React執行完當前渲染後,會立馬執行更新隊列裏的更新函數計算最新的state

  • 若是state值不變,則不會觸發re-render
  • 若是state值發生變化,則轉到懶計算策略。

當上一次計算的state沒有發生變化或者上次是初始state(說明React默認採用當即計算策略),則採用當即執行策略調用更新函數:

1. 當前state是初始state;

function Counter() {
    console.log('Counter.render begin');
    const [counter, setCounter] = useState(1);
    return (
           <>
               <p>counter={counter}</p>
               <button onClick={() => {                   
                    console.log('Click event begin');
                    setCounter(() => {
                        console.log('update');
                        return counter;
                    })
                    console.log('Click event end');
               }}>setCounter</button>
           </>
   )
}

點擊「setCounter」按鈕看下輸出:

Click event begin
update
Click event end

這樣說明了React默認採用當即執行策略。

2. 上一次計算state不變

function Counter() {
    console.log('Counter.render begin');
    const [counter, setCounter] = useState(1);
    return (
           <>
               <p>counter={counter}</p>     

               <button onClick={() => {                   
                    console.log('Click event begin');
                    // 保持state不變
                    setCounter(() => {
                        console.log('update');
                        return counter;
                    })
                    console.log('Click event end');
               }}>setCounter</button>
                <button onClick={() => {
                    setCounter(2)
                }}>setCounter2</button>
           </>
   )
}

先點擊兩次或者更屢次"setCounter2"按鈕(營造上次計算結果是state不變),再點擊一次「setCounter」按鈕看下輸出。

4.2 懶計算

懶計算就是上面說到的那樣。懶計算過程當中若是發現最終計算的state沒有發現變化,則React不選擇組件的子組件,即此時雖然執行了組件渲染函數,可是不會渲染組件的子組件

function Display({ counter }) {
    console.log('Display.render', counter);
    return <p>{counter}</p>
}

function Counter() {
    console.log('Counter.render begin');
    const [counter, setCounter] = useState(1);
    return (
           <>
               <Display counter={counter} />
               <button onClick={() => setCounter(2) }>setCounter2</button>
           </>
   )
}

點擊兩次「setCounter2」按鈕,看下輸出:

Counter.render begin
Display.render 2
Counter.render begin

第二次點擊雖然觸發了父組件re-render,可是子組件Display並無re-render

懶計算致使的問題只是會多觸發一次組件re-render,但這通常不是問題。React useState API文檔也提到了:

Note that React may still need to render that specific component again before bailing out. That shouldn’t be a concern because React won’t unnecessarily go 「deeper」 into the tree. If you’re doing expensive calculations while rendering, you can optimize them with useMemo.

4.3 當即計算自動轉懶計算

在一個批處理中採用當即計算髮現state發生變化,則立馬轉成懶計算模式,即後面的全部任務隊列的全部更新函數都不執行了。

function Counter() {
    console.log('Counter.render begin');
    const [counter, setCounter] = useState(1);
    return (
           <>
               <p>counter={counter}</p>     

               <button onClick={() => {                   
                    console.log('Click event begin');
                    // 保持state不變
                    setCounter(() => {
                        console.log('update 1');
                        return counter;
                    })

                    // state + 1
                    setCounter(() => {
                        console.log('update 2');
                        return counter + 1;
                    })

                    // state + 1
                    setCounter(() => {
                        console.log('update 3');
                        return counter + 1;
                    })
                    console.log('Click event end');
               }}>setCounter</button>
           </>
   )
}

點擊「setCounter」按鈕,看下輸出:

Click event begin // 先調用事件處理函數
update 1 // 上個state是初始state,採用當即執行策略,因此立馬執行更新函數1
update 2 // 更新函數1並無更新state,繼續採用當即執行策略,因此立馬執行更新函數2,可是state發生了變化,轉懶計算策略
Click event end
Counter.render begin
update 3

執行完更新函數2state發生了變化,React立馬轉成懶加載模式,後面的更新函數都不當即執行了。

4.4 從新認識跳過更新

什麼是跳過更新

  1. 不會渲染子組件;
  2. 不會觸發組件effect回調。
  3. 可是跳過更新並不表示不會從新執行渲染函數(從上面得知)

什麼狀況下會跳過更新

除了上面提到的state沒有發生變化時會跳過更新,還有當渲染函數裏調用setState/dispatch時也會觸發跳過更新。

function Display({ counter }) {
    console.log('Display.render', counter);
    return <p>{counter}</p>
}

export default function Counter() {
    const [counter, setCounter] = useState(0);
    console.log(`Counter.render begin counter=${counter}`);
    
    if(counter === 2) {
        setCounter(3)
    }
    
    useEffect(() => {
        console.log(`useEffect counter=${counter}`)
    }, [counter])

    return (
           <>
               <Display counter={counter}/>
               <button onClick={() => {                   
                    setCounter(2)
               }}>setCounter 2</button>
           </>
   )
}

點擊setCounter 2按鈕輸出:

Counter.render begin counter=2
Counter.render begin counter=3
Display.render 3
useEffect counter=3

能夠看到state=2觸發的更新被跳過了。

5、總結下

  1. 任務隊列是爲了懶計算更新函數;
  2. 批處理是爲了控制並觸發re-render
  3. 懶計算當即計算是爲了優化性能,既要實現state不變時不從新渲染組件,又要實現懶計算state

整理自GitHub筆記:解密React state hook

相關文章
相關標籤/搜索