先看个问题,下面组件中若是点击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
。