先看个问题,下面组件中如果点击3次组件Counter
的“setCounter”按钮,控制台输出是什么?
function Display({ counter }) {
console.log('Display.render', counter);
return <p>{counter}</p>
}
function Counter() {
const [counter, setCounter] = useState(1);
console.log('Counter.render', counter);
return (
<>
<Display counter={counter}/>
<button onClick={() => setCounter(2)}>setCounter</button>
</>
)
}
正确的答案:
第一次点击“setCounter”按钮,
state
变成2,触发一次re-render。输出:Counter.render 2 Display.render 2
第二次点击“setCounter”按钮,虽然
state
没有变,但是又触发了一次组件Counter
re-render,但是没有触发组件Display
re-render,输出:Counter.render 2 Display.render 2 Counter.render 2
- 第三次点击“setCounter”按钮,
state
没有变,也没有触发re-render。
如果答对了,可以直接先点赞并返回上一页了。如果没有答对,那得花点时间读读剩下的内容了
一、更新队列
其实每个state hook都关联一个更新队列。每次调用setState
函数时,React并不会立即执行更新函数,而是把更新函数插入更新队列里,并告诉React需要安排一次re-render。
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
执行事件处理函数过程中并没有执行更新函数。主要还是为了性能优化吧,因为可能存在多处setState
函数调用。
1.2 多个任务队列
每个state hook对应一个任务队列,一个组件里可能会涉及多个任务队列。
- 每个任务队列是互相独立的;
- 每个任务队列的更新函数执行顺序取决于任务队列创建先后,即调用
useState
的先后顺序。
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>
</>
)
}
setCounter
对应的任务队列的更新函数永远要先于setCounter2
对应的任务队列的更新函数执行。
二、懒计算
只有需要state
时React才会去计算最新的state
值,即得等到再次执行useState
时才会执行更新队列里的更新函数。并且同一个更新队列里多个更新函数是依次执行的,前一个更新函数的输出,作为下一个更新函数的输入。
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
会发现此时先执行的渲染函数,再执行更新函数。第二个更新函数的实参就是第一个更新函数的返回值。
三、批处理
只有再次执行useState
时React才会执行更新函数,也就是说只有再次执行渲染函数时才会知道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
触发两次批处理。
四、跳过渲染
我们都知道如果state
的值没有发生变化,React是不会重新渲染组件的。但是从上面得知React只有再次执行useState
时才会计算state
的值啊。
为了计算最新的state
需要触发re-render,而state
如果不变又不渲染组件,这好像是个先有蛋还是先有鸡的问题。React是采用2个策略跳过重新渲染。
4.1 立即计算
上面提到的都是懒计算,其实React还存在立即计算。React立即更新函数:
- 如果
state
值不变,则不会触发re-render
; - 如果
state
值发生变化,则转到懒加载策略。
当上一次计算的state
没有发生变化或者上次是初始state
,则采用立即执行策略调用更新函数
当前
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”按钮看下输出。
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
执行完更新函数2
时state
发生了变化,React立马转成懒加载模式,后面的更新函数都不立即执行了。
参考
整理自gitHub笔记:
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。