先看个问题,上面组件中如果点击 3 次组件 Counter
的“setCounter”按钮,控制台输入是什么?
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>
}
.
.
.
正确的答案是:
-
第一次点击“setCounter”按钮,
state
的值变成 2 触发一次re-render
;
即输入:Counter.render 2 Display.render 2
-
第二次点击“setCounter”按钮,尽管
state
的值没有变,但也触发了一次组件Counter
re-render
,然而没有触发组件Display
re-render
;
即输入:Counter.render 2
- 第三次点击“setCounter”按钮,
state
没有变,也没有触发re-render
。
一、更新队列
1.1 什么是更新队列
其实每个 state hook 都关联一个 更新队列 。每次调用setState
/dispatch
函数时,React 并不会立刻执行 state
的更新函数,而是把更新函数插入更新队列里,并通知 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
通过例子能够看出在执行事件处理函数过程中并没有立刻执行 state
更新函数。这次要是为了性能优化,因为可能存在多处 setState
/dispatch
函数调用。
1.2 多个更新队列
每个 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
。
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 总结
- 能够触发批处理的回调函数:
- React 事件处理函数;
- React 生命周期函数,如
useEffect
副作用函数; - 组件渲染函数外部
在实现getDerivedStateFromProps
中会遇到这种调用场景。 - 不会触发批处理的回调函数:
非 React 触发调用的回调函数,比方setTimeout/setInterval
等异步处理函数
四、跳过更新
咱们都晓得如果 state
的值没有发生变化,React 是不会从新渲染组件的。然而从下面得悉 React 只有再次执行 useState
时才会计算 state
的值啊。
为了计算最新的 state
须要触发 re-render,而 state
如果不变又不渲染组件,这如同是个先有蛋还是先有鸡的问题。React 是采纳 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
执行完 更新函数 2
时 state
产生了变动,React 立马转成懒加载模式,前面的更新函数都不立刻执行了。
4.4 重新认识跳过更新
什么是跳过更新
- 不会渲染子组件;
- 不会触发组件
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
。
整顿自 GitHub 笔记:解密 React state hook