资料来源:掘金课程 https://juejin.cn/book/6945998773818490884?enter_from=course_center&utm_source=course_center
记录一些笔记
事件合成
React的事件其实是React重新实现的一套事件系统。目标是统一管理事件,提供一种跨浏览器一致性的事件处理方式。
元素绑定事件并不是原生事件,而是React合成的事件,所谓的“合成”,是指你用React添加的一个事件,在真正的dom元素中,可能是1对多的,比如为<input>
绑定一个onChange事件,是由blur, change, focus等多个事件合成的。最后事件对象经过不同的事件插件处理后,统一绑定到顶层容器上,这个顶层容器,V17之前是document,V17是app容器。
State( Legacy模式下的state)
在不同的React模式下,state的更新流程是不同的
React的模式包括:
legacy
模式:平时使用比较多的模式blocking
模式:可以视为concurrent的优雅降级版本和过渡版本concurrent
模式:V18
1 类组件中的state
类组件中的setState()
方法来更新state
setState(obj, callback)
- 第一个参数:
(1)obj为一个对象,就是即将合并的state
(2)obj是一个函数,function(state,props){ return {/* 合并新的state*/}}
- 第二个参数:
state更新后的副作用函数,可以获取当前setState更新后的最新state值,做一些操作
/* 第一个参数为function类型 */
this.setState((state,props)=>{
return { number:1 }
})
/* 第一个参数为object类型 */
this.setState({ number:1 },()=>{
console.log(this.state.number) //获取最新的number
})
限制state更新视图的方式:
pureComponent
可以对 state 和 props 进行浅比较,如果没有发生变化,那么组件不更新shouldComponentUpdate
生命周期可以通过判断前后 state 变化来决定组件需不需要更新,需要更新返回true,否则返回false。
之前说过,类组件的setState实际调用的是Updater对象上的enqueueSetState
方法,所以想知道底层是如何运行的可以看一下精简版源码
// react-reconciler/src/ReactFiberClassComponent.js
enqueueSetState(){
/* 每一次调用`setState`,react 都会创建一个 update 里面保存了 */
const update = createUpdate(expirationTime, suspenseConfig);
/* callback 可以理解为 setState 回调函数,第二个参数 */
callback && (update.callback = callback)
/* enqueueUpdate 把当前的update 传入当前fiber,待更新队列中 */
enqueueUpdate(fiber, update);
/* 开始调度更新 */
scheduleUpdateOnFiber(fiber, expirationTime);
}
所以每个fiber对象的更新,会放到对应的fiber对象的一个待更新队列中,最后开启调度更新,进入到React底层 做的这些事?
- setState产生当前更新的优先级
- 从fiber Root根部 fiber 向下调和子节点,对比发生更新的地方,找到发生更新的组件
- 在这些组件中合并state,然后触发render函数,得到新的UI试图层,完成render阶段
- 到commit阶段:替换真实的DOM。
- 仍在commit阶段,执行
setState
中的callback函数
第3步中提到了“合并state”,这与批量更新有关,批量更新batchUpdate则与事件系统息息相关。React采用事件合成的形式
/* 源码 react-dom/src/events/DOMLegacyEventPluginSystem.js */
/* 在`legacy`模式下,所有的事件都将经过此函数同一处理 */
function dispatchEventForLegacyPluginEventSystem(){
/** !!! 下面来重点看这个批量事件更新函数
* handleTopLevel 事件处理函数
*/
batchedEventUpdates(handleTopLevel, bookKeeping); //
}
/* 源码 react-dom/src/events/ReactDOMUpdateBatching.js */
function batchedEventUpdates(fn,a){
/* 开启批量更新 */
isBatchingEventUpdates = true;
try {
/* 这里执行了的事件处理函数, 比如在一次点击事件中触发setState,那么它将在这个函数内执行 */
return batchedEventUpdatesImpl(fn, a, b);
} finally {
/* try 里面 return 不会影响 finally 执行 */
/* 完成一次事件批量更新, 关闭开关 */
isBatchingEventUpdates = false;
}
}
举个例子,下面组件中,点击一次<button>
,调用了三次setState
export default class index extends React.Component{
state = { number:0 }
handleClick= () => {
// 下面的三次setState传入的newStateObj,会被合并
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)
// 控制台输出:
// 0, 0, 0, callback1 1 ,callback2 1 ,callback3 1
}
render(){
return <div>
{ this.state.number }
<button onClick={ this.handleClick } >number++</button>
</div>
}
}
在整个React上下文执行栈中会变成这样
批量更新的规则可以被打破吗?我如果不想让他合并呢?那就可以使用异步操作,比如promise
或setTimeout
// 比如 handleClick 这么写
handleClick = (){
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上下文执行栈中会变成:
但如果我也在异步环境下,也使用批量更新的模式,应该怎么做呢?
可以使用ReactDOM的批量更新方法unstable_bachedUpdates
, 手动批量更新
handleClick = (){
setTimeout(()=>{
unstable_bachedUpdates(() => {
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)
// 控制台输出:0 , 0 , 0 , callback1 1 , callback2 1 ,callback3 1
})
})
}
如果要改变优先级,可以使用flushSync
,React同一级别更新优先级关系是:
flushSync中的setState > 正常执行上下文中的 setState > setTimtout/Promise中的 setState
flushSync补充:flushSync在同步条件下,会合并正常执行上下文中的setState,因此下面的2
被合并了
handerClick=()=>{
setTimeout(()=>{
this.setState({ number: 1 })
})
this.setState({ number: 2 })
ReactDOM.flushSync(()=>{
this.setState({ number: 3 })
})
this.setState({ number: 4 })
// 输出: 3 4 1
}
render(){
console.log(this.state.number)
return ...
}
2 函数组件中的state
[ ①state , ②dispatch ] = useState(③initData)
initDate参数:
- state初始值
- 或函数:返回值作为state初始值
dispatch函数的入参:
- 直接传入newState的值
- 或函数:
(旧state)=>(/*新state*/)
返回值作为newState值
注意:
- 当调用改变 state 的函数dispatch,在本次函数执行上下文中,是获取不到最新的 state 值的。原因:函数组件更新就是函数的执行,在函数一次执行过程中,函数内部所有变量重新声明,所以只有在下一次函数组件执行时,state才会被更新为新的值。所以在同一个函数执行上下文中,state还是原来的值。
- 那应该如何监听state变化?
A:在函数组件中,一般使用useEffect
监听state的变化
- 那应该如何监听state变化?
- 在useState的dispatchAction中,不要使用相同的state(地址相同的state),需要浅拷贝一份state作为newState的值。因为在dispatchAction的处理逻辑中,会对state进行浅比较,如果两次state指向相同的内存空间,会默认state相等,就不会发生视图更新了。所以一般使用
dispatchState({...state})
。因为...
会浅拷贝一份
比较类组件的setState和函数组件的useState的异同点
相同点:
- 都更新了视图,底层都调用了
scheduleUpdateOnFiber
方法,而且事件驱动情况下都有批量更新规则。
不同点: - 在不是pureComponent组件模式下,setState不会浅比较两次state的值,只有调用setState,就会执行更新。但是useState中的dispatch 会默认比较两次state是否相同,然后决定是否更新组件。
- setState有callback;但是函数组件中,智能通过useEffect来执行state变化引起的副作用。
- setState在底层逻辑上主要是和老state进行合并处理,而useState更倾向于重新赋值。