React 有两个重要阶段,render 阶段和 commit 阶段,React 在调和( render )阶段会深度遍历React fiber树,目的就是发现不同(diff),不同的地方就是接下来需要更新的地方,对于变化的组件,就会执行render函数。在一次调和过程完毕之后,就到了commit 阶段,commit 阶段会创建修改真实的 DOM 节点。
setState
/* 第一个参数为function类型 */
this.setState((state,props)=>{
return { number:1 }
})
/* 第一个参数为object类型 */
this.setState({ number:1 },()=>{
console.log(this.state.number) //获取最新的number
})
假如一次事件中触发一次如上 setState ,在 React 底层主要做了那些事呢?
- 首先,setState 会产生当前更新的优先级(expirationTime)
- 接下来 React 会从 fiber Root 根部 fiber 向下调和子节点,调和阶段将对比发生更新的地方,更新对比 expirationTime ,找到发生更新的组件,合并 state,然后触发 render 函数,得到新的 UI 视图层,完成 render 阶段。
- commit 阶段,替换真实 DOM ,完成此次更新流程。
- 此时仍然在 commit 阶段,会执行 setState 中 callback 函数,如上的()=>{ console.log(this.state.number) },到此为止完成了一次 setState 全过程。
更新流程如下图:
setState原理
在第二章component中,讲到类组件初始化过程中绑定了负责更新的Updater对象。调用 setState 方法,实际上是 React 底层调用 Updater 对象上的 enqueueSetState 方法。
enqueueSetState(){
/* 每一次调用`setState`,react 都会创建一个 update 里面保存了 */
const update = createUpdate(expirationTime, suspenseConfig);
/* callback 可以理解为 setState 回调函数,第二个参数 */
callback && (update.callback = callback)
/* enqueueUpdate 把当前的update 传入当前fiber,待更新队列中 */
enqueueUpdate(fiber, update);
/* 开始调度更新 */
scheduleUpdateOnFiber(fiber, expirationTime);
}
Q:setState到底是同步的还是异步的
从React底层代码来讲,这个问题涉及到batchedEventUpdates批量更新概念,以及批量更新被打破的条件。
function batchedEventUpdates(fn,a){
/* 开启批量更新 */
isBatchingEventUpdates = true;
try {
/* 这里执行了的事件处理函数, 比如在一次点击事件中触发setState,那么它将在这个函数内执行 */
return batchedEventUpdatesImpl(fn, a, b);
} finally {
/* try 里面 return 不会影响 finally 执行 */
/* 完成一次事件,批量更新 */
isBatchingEventUpdates = false;
}
}
如上可以分析出流程,在 React 事件执行之前通过 isBatchingEventUpdates=true 打开开关,开启事件批量更新,当该事件结束,再通过 isBatchingEventUpdates = false; 关闭开关,然后在 scheduleUpdateOnFiber 中根据这个开关来确定是否进行批量更新。
看下面的例子:
export default class index extends React.Component{
state = { number:0 }
handleClick= () => {
this.setState({ number:this.state.number + 1 },()=>{ console.log( 'callback1', this.state.number) })
console.log(this.state.number)
this.setState({ number:this.state.number + 1 },()=>{ console.log( 'callback2', this.state.number) })
console.log(this.state.number)
this.setState({ number:this.state.number + 1 },()=>{ console.log( 'callback3', this.state.number) })
console.log(this.state.number)
}
render(){
return <div>
{ this.state.number }
<button onClick={ this.handleClick } >number++</button>
</div>
}
}
// 点击打印:0, 0, 0, callback1 1 ,callback2 1 ,callback3 1。
如上代码,在整个 React 上下文执行栈中会变成这样:
为什么异步操作里面的批量更新规则会被打破呢
setTimeout(()=>{
this.setState({ number:this.state.number + 1 },()=>{ console.log( 'callback1', this.state.number) })
console.log(this.state.number)
this.setState({ number:this.state.number + 1 },()=>{ console.log( 'callback2', this.state.number) })
console.log(this.state.number)
this.setState({ number:this.state.number + 1 },()=>{ console.log( 'callback3', this.state.number) })
console.log(this.state.number)
})
// 打印callback1 1 , 1, callback2 2 , 2,callback3 3 , 3
在整个 React 上下文执行栈中就会变成如下图这样:
如何在如上异步环境下,继续开启批量更新模式呢
React-Dom 中提供了批量更新方法 unstable_batchedUpdates,可以去手动批量更新。
setTimeout(()=>{
unstable_batchedUpdates(()=>{
this.setState({ number:this.state.number + 1 })
console.log(this.state.number)
this.setState({ number:this.state.number + 1})
console.log(this.state.number)
this.setState({ number:this.state.number + 1 })
console.log(this.state.number)
})
})
// 输出 0 , 0 , 0 , callback1 1 , callback2 1 ,callback3 1
在实际工作中,unstable_batchedUpdates 可以用于 Ajax 数据交互之后,合并多次 setState,或者是多次 useState 。
提升更新优先级
React-dom 提供了 flushSync,flushSync可以将回调函数中的更新任务,放在一个较高的优先级中。
handerClick=()=>{
setTimeout(()=>{
this.setState({ number: 1 })
})
this.setState({ number: 2 })
ReactDOM.flushSync(()=>{
this.setState({ number: 3 })
})
this.setState({ number: 4 })
}
render(){
console.log(this.state.number)
return ...
}
// 输出3 4 1
flushSync补充说明:flushSync 在同步条件下,会合并之前的 setState | useState,可以理解成,如果发现了 flushSync ,就会先执行更新,如果之前有未更新的 setState | useState ,就会一起合并了,所以就解释了如上,2 和 3 被批量更新到 3 ,所以 3 先被打印。
React 同一级别更新优先级关系是:
flushSync 中的 setState > 正常执行上下文中 setState > setTimeout ,Promise 中的 setState。
useState
基本用法
[ ①state , ②dispatch ] = useState(③initData)
对于 dispatch的参数,也有两种情况:
-
第一种非函数情况,此时将作为新的值,赋予给 state,作为下一次渲染使用;
-
第二种是函数的情况,如果dispatch的参数为一个函数,这里可以称它为reducer,reducer 参数,是上一次返回最新的 state,返回值作为新的 state。
// disoatch 参数是一个函数
const [ number , setNumbsr ] = React.useState(0)
const handleClick=()=>{
setNumber((state)=> state + 1) // state - > 0 + 1 = 1
setNumber(8) // state - > 8
setNumber((state)=> state + 1) // state - > 8 + 1 = 9
}
注意:当调用改变 state 的函数dispatch,在本次函数执行上下文中,是获取不到最新的 state 值的。因为函数组件更新就是函数的执行,在函数一次执行过程中,函数内部所有变量重新声明,所以改变的 state ,只有在下一次函数组件执行时才会被更新。
useState原理
见后面章节
setState和useState的区别
- 相同点
从底层来看,setState和 useState 都调用了 scheduleUpdateOnFiber 方法,而且事件驱动情况下都有批量更新规则。 - 不同点
- setState 的 callback函数可以获取最新state;但是在函数组件中,只能通过 useEffect 来执行 state 变化引起的副作用。
- setState 在底层处理逻辑上主要是和老 state 进行合并处理,而 useState 更倾向于重新赋值。
本文学习自掘金小册《React进阶实践指南》