Let’s look at the question first. If you click the "setCounter" button of the Counter
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>
}
.
.
.
The correct answer is:
Click the "setCounter" button for the first time, and
state
becomes 2 to triggerre-render
once;
That is to output:Counter.render 2 Display.render 2
Second click "setCounter" button, although
state
value has not changed, but also triggered a componentCounter
re-render
, but did not trigger assemblyDisplay
re-render
;
That is to output:Counter.render 2
- Clicking the "setCounter" button for the third time,
state
remains unchanged andre-render
is not triggered.
One, update the queue
1.1 What is an update queue
In fact, each state hook is associated with a update queue . Every time the setState
/ dispatch
function is called, React does not immediately execute state
, but inserts the update function into the update queue and tells React to schedule re-render
.
Give a chestnut:
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>
</>
)
}
first click the "Add" button (the reason will be explained later), and then click the "setCounter" button to see the output:
Click event begin
Click event end
update 1
update 2
Counter.render 2
Display.render 2
Through the example, it can be seen that the state
update function is not immediately executed during the execution of the event processing function. This is mainly for performance optimization, because there may be multiple setState
/ dispatch
function calls.
1.2 Multiple update queues
Each state
corresponds to an update queue, and a component may involve multiple update queues.
- Each update queue is independent of each other;
- The execution order of the update function of each update queue depends on the order in which the task queue is created (that is, the order in
useState/useReducer
- Multiple update functions in the same update queue are executed in sequence, and the output of the previous update function is used as the input of the next update function (similar to a pipeline).
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>
</>
)
}
Click the "setCounter2" button to see the output result. The example setCounter
corresponding update queues always precede the update function setCounter2
update function corresponding to the task execution queue.
Two, lazy calculation
When will the update function of the update queue be executed? Lazy calculation is one of the strategies for executing update functions. Lazy calculation means that React will calculate the latest state
state
is needed. That is, it will not execute the update function in the update queue 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>
</>
)
}
Click the "Add" button first, and then click the "setCounter" button to see the output:
Click event begin
Click event end
Counter.render begin
update 1, prev=1
update 2, prev=10
Counter.render 20
Display.render 20
Through chestnuts, you will find:
- Execute the rendering function first, and then execute the update function;
- The actual parameter of the second update function is the return value of the first update function.
Three, batch processing
In the lazy calculation, only when the rendering function is executed again will it be known state
has changed. Then when will React execute the component rendering function again?setState
in the event processing function, and React executes the event processing function in a batch. After the event processing function is executed, if the re-render
request is triggered (one or more times), React will once and only trigger re-render
once.
3.1 Features
1. A batch process can trigger re-render
most once, and a batch process can contain multiple update queues;
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>
</>
)
}
Click the "setCounter" button and see the output:
Counter.render begin
2. Batch processing can only process the synchronous code in the callback function, and the asynchronous code will be treated as a new batch;
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>
</>
)
}
Click the "setCounter" button and see the output:
Counter.render begin
Display.render 10
Counter.render begin
Display.render 20
Trigger two batches.
re-render
triggered in the asynchronous callback function will not be processed as a batch
setTimeout/setInterval
are not triggered by React, and React cannot perform batch processing 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>
</>
)
}
Click the setCounter button to output:
Counter.render begin
Display.render 11
Counter.render begin
Display.render 20
Counter.render begin
Display.render 21
In two event handlers can be seen setState
were batch, and setTimeout
callback function two setState
were fired twice re-render.
3.2 Summary
- The callback function that can trigger batch processing:
- React event handling function;
- React life cycle functions, such as
useEffect
side effect functions; - Inside the component rendering function
This kind of call scenario will be encountered in the implementation ofgetDerivedStateFromProps
- Callback functions that will not trigger batch processing:
Callback functions called by non-React triggers, such as asynchronous processing functionssetTimeout/setInterval
Four, skip the update
We all know that if state
does not change, React will not re-render the component. But that is only executed once again from the above React useState
will be calculated when state
value ah.
In order to calculate the latest state
necessary to trigger the re-render, and state
does not change without rendering the component, this seems to be a question of whether the egg comes first or the chicken comes first. React uses 2 strategies to skip re-rendering:
- Lazy calculation
- Calculate immediately
4.1 Calculate immediately
In addition to the lazy calculations mentioned above, in fact, React also has immediate calculations. When React finishes performing the current rendering, it will immediately execute the update function in the update queue to calculate the latest state
:
- If the
state
does not change,re-render
will not be triggered; - If the
state
changes, switch to the lazy calculation strategy.
When the last calculated state
did not change or the last time was the initial state
(indicating that React uses the immediate calculation strategy by default), the immediate execution strategy is used to call the update function:
1. The current state
is the initial 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>
</>
)
}
Click the "setCounter" button to see the output:
Click event begin
update
Click event end
This shows that React uses an immediate execution strategy by default.
2. The last calculation is state
unchanged
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>
</>
)
}
Click the "setCounter2" button twice or more times (make the last calculation result is state
unchanged), then click the "setCounter" button again to see the output.
4.2 Lazy calculation
Lazy calculations are as mentioned above. During the lazy calculation process, if state
, React does not select the component's sub-component, that is, although the component rendering function is executed at this time, the component's sub-component will not be rendered.
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>
</>
)
}
Click the "setCounter2" button twice to see the output:
Counter.render begin
Display.render 2
Counter.render begin
Although the second click triggered the parent component re-render
, but the child component Display
did not have re-render
.
The problem caused by lazy calculation is that the component re-render
will be triggered one more time, but this is generally not a problem. React useState
API document also mentions:
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 Calculate immediately and automatically switch to lazy calculation
state
has changed by using immediate calculation, it immediately switches to lazy calculation mode, that is, all update functions of all subsequent task queues are not executed.
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>
</>
)
}
Click the "setCounter" button to see the output:
Click event begin // 先调用事件处理函数
update 1 // 上个state是初始state,采用立即执行策略,所以立马执行更新函数1
update 2 // 更新函数1并没有更新state,继续采用立即执行策略,所以立马执行更新函数2,但是state发生了变化,转懒计算策略
Click event end
Counter.render begin
update 3
After executing update function 2,
state
changed, React immediately turned into lazy loading mode, and subsequent update functions were not executed immediately.
4.4 Recognition skip update
What is skip update
- Does not render child components;
- Will not trigger component
effect
callback. - but skipping the update does not mean that the rendering function will not be re-executed (learned from the above)
Under what circumstances will the update be skipped
In addition to the above mentioned state
, the update will be skipped when there is no change, and the skip update will also be triggered when 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>
</>
)
}
Click the setCounter 2 button to output:
Counter.render begin counter=2
Counter.render begin counter=3
Display.render 3
useEffect counter=3
You can see that the update triggered by state=2
Five, summary
- task queue is for lazy calculation of update function;
- batch processing is to control and trigger
re-render
; - lazy calculation and immediately calculate is to optimize performance, not only to achieve
state
do not re-render components when unchanged, but also to achieve lazy calculationstate
.
from the 1611126e1e8198 GitHub note: Decrypting React state hook
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。