4

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:

  1. Click the "setCounter" button for the first time, and state becomes 2 to trigger re-render once;
    That is to output:

    Counter.render 2
    Display.render 2
  2. Second click "setCounter" button, although state value has not changed, but also triggered a component Counter re-render , but did not trigger assembly Display re-render ;
    That is to output:

    Counter.render 2
  3. Clicking the "setCounter" button for the third time, state remains unchanged and re-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.

  1. Each update queue is independent of each other;
  2. 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
  3. 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:

  1. Execute the rendering function first, and then execute the update function;
  2. 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

  1. The callback function that can trigger batch processing:
  2. React event handling function;
  3. React life cycle functions, such as useEffect side effect functions;
  4. Inside the component rendering function
    This kind of call scenario will be encountered in the implementation of getDerivedStateFromProps
  5. Callback functions that will not trigger batch processing:
    Callback functions called by non-React triggers, such as asynchronous processing functions setTimeout/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:

  1. Lazy calculation
  2. 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

  1. Does not render child components;
  2. Will not trigger component effect callback.
  3. 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

  1. task queue is for lazy calculation of update function;
  2. batch processing is to control and trigger re-render ;
  3. 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 calculation state .

from the 1611126e1e8198 GitHub note: Decrypting React state hook


普拉斯强
2.7k 声望53 粉丝

Coder