先看個問題,下面組件中若是點擊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
第一次點擊「setCounter」按鈕,state
的值變成2觸發一次re-render
;
即輸出:git
Counter.render 2 Display.render 2
第二次點擊「setCounter」按鈕,雖然state
的值沒有變,但也觸發了一次組件Counter
re-render
,可是沒有觸發組件Display
re-render
;
即輸出:github
Counter.render 2
state
沒有變,也沒有觸發re-render
。其實每一個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
函數調用。異步
每一個state
都對應一個更新隊列,一個組件裏可能會涉及多個更新隊列。函數
useState/useReducer
的前後順序)。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
對應的任務隊列的更新函數執行。性能
何時執行更新隊列的更新函數呢?懶計算就是執行更新函數的策略之一。懶計算是指只有須要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
經過栗子會發現:
在懶計算中只有再次執行渲染函數時纔會知道state
是否發生變化。那React何時再次執行組件渲染函數呢?
通常咱們都是在事件處理函數裏調用setState
,React在一個批處理裏執行事件處理函數。事件處理函數執行完畢後若是觸發了re-render
請求(一次或者屢次),則React就觸發一次且只觸發一次re-render
。
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
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
觸發兩次批處理。
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。
useEffect
反作用函數;getDerivedStateFromProps
中會遇到這種調用場景。setTimeout/setInterval
等異步處理函數咱們都知道若是state
的值沒有發生變化,React是不會從新渲染組件的。可是從上面得知React只有再次執行useState
時纔會計算state
的值啊。
爲了計算最新的state
須要觸發re-render,而state
若是不變又不渲染組件,這好像是個先有蛋仍是先有雞的問題。React是採用2個策略跳太重新渲染:
除了上面提到的都是懶計算,其實React還存在當即計算。當React執行完當前渲染後,會立馬執行更新隊列裏的更新函數計算最新的state
:
state
值不變,則不會觸發re-render
;state
值發生變化,則轉到懶計算策略。當上一次計算的state
沒有發生變化或者上次是初始state
(說明React默認採用當即計算策略),則採用當即執行策略調用更新函數:
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默認採用當即執行策略。
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」按鈕看下輸出。
懶計算就是上面說到的那樣。懶計算過程當中若是發現最終計算的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.
在一個批處理中採用當即計算髮現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
執行完更新函數2
時state
發生了變化,React立馬轉成懶加載模式,後面的更新函數都不當即執行了。
effect
回調。除了上面提到的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
觸發的更新被跳過了。
re-render
;state
不變時不從新渲染組件,又要實現懶計算state
。