解密React state hook

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

  1. 第一次点击“setCounter”按钮,state的值变成2触发一次re-render
    即输出:git

    Counter.render 2
    Display.render 2
  2. 第二次点击“setCounter”按钮,虽然state的值没有变,但也触发了一次组件Counter re-render,可是没有触发组件Display re-render
    即输出:github

    Counter.render 2
  3. 第三次点击“setCounter”按钮,state没有变,也没有触发re-render

1、更新队列

1.1 什么是更新队列

其实每一个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函数调用。异步

1.2 多个更新队列

每一个state都对应一个更新队列,一个组件里可能会涉及多个更新队列。函数

  1. 各个更新队列是互相独立的;
  2. 各个更新队列的更新函数执行顺序取决于任务队列建立前后(即调用useState/useReducer的前后顺序)。
  3. 同一个更新队列里多个更新函数是依次执行的,前一个更新函数的输出做为下一个更新函数的输入(相似管道)。
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对应的任务队列的更新函数执行。性能

2、懒计算

何时执行更新队列的更新函数呢?懒计算就是执行更新函数的策略之一。懒计算是指只有须要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

经过栗子会发现:

  1. 先执行渲染函数,再执行更新函数;
  2. 第二个更新函数的实参就是第一个更新函数的返回值。

3、批处理

在懒计算中只有再次执行渲染函数时才会知道state是否发生变化。那React何时再次执行组件渲染函数呢?
通常咱们都是在事件处理函数里调用setState,React在一个批处理里执行事件处理函数。事件处理函数执行完毕后若是触发了re-render请求(一次或者屡次),则React就触发一次且只触发一次re-render

3.1 特性

1. 一个批处理最多触发一次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

2. 批处理只能处理回调函数里的同步代码,异步代码会做为新的批处理;

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

触发两次批处理。

3. 异步回调函数里触发的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。

3.2 总结

  1. 能够触发批处理的回调函数:
  2. React事件处理函数;
  3. React生命周期函数,如useEffect反作用函数;
  4. 组件渲染函数内部
    在实现getDerivedStateFromProps中会遇到这种调用场景。
  5. 不会触发批处理的回调函数:
    非React触发调用的回调函数,好比setTimeout/setInterval等异步处理函数

4、跳过更新

咱们都知道若是state的值没有发生变化,React是不会从新渲染组件的。可是从上面得知React只有再次执行useState时才会计算state的值啊。
为了计算最新的state须要触发re-render,而state若是不变又不渲染组件,这好像是个先有蛋仍是先有鸡的问题。React是采用2个策略跳太重新渲染:

  1. 懒计算
  2. 当即计算

4.1 当即计算

除了上面提到的都是懒计算,其实React还存在当即计算。当React执行完当前渲染后,会立马执行更新队列里的更新函数计算最新的state

  • 若是state值不变,则不会触发re-render
  • 若是state值发生变化,则转到懒计算策略。

当上一次计算的state没有发生变化或者上次是初始state(说明React默认采用当即计算策略),则采用当即执行策略调用更新函数:

1. 当前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默认采用当即执行策略。

2. 上一次计算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

执行完更新函数2state发生了变化,React立马转成懒加载模式,后面的更新函数都不当即执行了。

4.4 从新认识跳过更新

什么是跳过更新

  1. 不会渲染子组件;
  2. 不会触发组件effect回调。
  3. 可是跳过更新并不表示不会从新执行渲染函数(从上面得知)

什么状况下会跳过更新

除了上面提到的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触发的更新被跳过了。

5、总结下

  1. 任务队列是为了懒计算更新函数;
  2. 批处理是为了控制并触发re-render
  3. 懒计算当即计算是为了优化性能,既要实现state不变时不从新渲染组件,又要实现懒计算state

整理自GitHub笔记:解密React state hook

相关文章
相关标签/搜索