React系列 之 React进阶 含源码解读 (一)事件合成、state原理

本文详细介绍了React事件系统如何统一管理事件,以及在Legacy模式和concurrent模式下state更新的不同流程。重点讨论了setState的使用、纯组件和非纯组件的state更新策略,以及批量更新和事件合成在React中的应用。
摘要由CSDN通过智能技术生成

资料来源:掘金课程 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底层 做的这些事?

  1. setState产生当前更新的优先级
  2. 从fiber Root根部 fiber 向下调和子节点,对比发生更新的地方,找到发生更新的组件
  3. 在这些组件中合并state,然后触发render函数,得到新的UI试图层,完成render阶段
  4. 到commit阶段:替换真实的DOM。
  5. 仍在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上下文执行栈中会变成这样
在这里插入图片描述
批量更新的规则可以被打破吗?我如果不想让他合并呢?那就可以使用异步操作,比如promisesetTimeout

// 比如 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的变化
  • 在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更倾向于重新赋值。
  • 10
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值