• 探究竟篇之React中的state


    1.类组件中的state

    setState的用法

    React项目中UI改变来源于state的改变,类组件中setState是更新组件,渲染视图的主要方式

    基本用法

    setState(obj,callback)
    • 第一个参数:当obj是一个对象,即为即将合并的state;如果obj是一个函数,那么当组件的state和props将作为参数,返回值用于合并新的state
    • 第二个参数callback:callback为一个函数,函数执行上下文中可以获取当前setState更新后的最新的值,可以作为依赖state变化的副作用函数,可以用来做一些基本的DOM操作
    1. /* 第一个参数为function类型 */
    2. this.setState((state,props)=>{
    3. return {number:}
    4. })
    5. /* 第一个参数为object类型 */
    6. this.setState({ number:1 },()=>{
    7. console.log(this.state.number) //获取最新的number
    8. })

    加入一次事件中触发一次如上的setState,在React底层主要做了哪些事呢?

    • 首先,setState会产生当前更新的优先级------产生更新优先级
    • 接下来React会从fiber Root根部fiber向下调和子节点,调和阶段将对比发生更新的彼方,更新对比expirationTime,找到发生更新的组件,合并state,然后触发render函数,得到最新的UI视图,完成render阶段--------对比
    • 接下来到commit阶段,commit阶段,替换真实DOM,完成此次更新流程。--------替换真实DOM
    • 此时仍然在commit阶段,会执行setState中callback函数,如上的()=>{console.log(this.state.number)} ,到此为止就完成了一次setState的过程。

    更新的流层图如下:

     要记住一个主要任务的先后顺序,这对于弄清渲染过程会有帮助:

    render阶段render函数执行--->commit阶段真实DOM替换--->setState回调函数执行callback

    类组件如何限制state更新视图

    对于类组件如何显示state带来的更新作用呢?

    • pureComponet可以对state和props进行浅比较,如果没有发生变化,那么组件就不会更新
    • shouldComponentUpdate生命周期可以通过判断前后state变化来决定组件需不需要更新,需要更新返回true,否则返回false

    setState原理揭秘

    知其然,知其所以然,下面将介绍setState的底层逻辑,要弄清楚state的更新机制,所以接下来要从两个方向分析

    • 一是揭秘enqueueSetState到底做了什么?
    • 二是React底层是如何进行批量更新的?

    首先,这里极简了一下enqueueSetState的代码,如下:

    1. enqueueSetState(){
    2. //每次调用setState,react都会创建一个update里面保存了如下
    3. const update= createUpdate(expirationTime,suspenseConfig)
    4. //callback 可以理解为setState回调函数,第二个参数
    5. callback && (update.callback=callback)
    6. //enqueuUpdate 把当前的update 传入当前fier ,待更新队列中
    7. enqueuUpdate(fiber,update)
    8. //开始调度更新
    9. scheduleUpdateOnFiber(fiber,expirationTime)
    10. }

    enqueueSetState作用实际很简单,就是创建一个update,然后放入当前的fiber对象的待更新队列中,最后开启调度更新,进入上述讲到的更新流程。

    那么问题来了,React的batchUpdate批量更新是什么时候加上去的呢?

    这就要提前聊到事件系统了,正常的state更新,UI交互,都离不开用户的事件,比如点击事件,表单输入等,React是采用事件合成的形式,每一个事件都是由React事件系统统一调度的,那么State批量更新正是和事件系统息息相关的。

    1. //在legcy模式下,所有的事件都将经过此函数统一处理
    2. function dispatchEventFoLegacyPluginEventSystem(){
    3. //handleTopLevel 事件处理函数
    4. batchEventUpdates(handleTopLvele,bookKeeping)
    5. }

    batchEventUpdates方法具体如下:

    1. batchEventUpdate(fn,a){
    2. //开启批量更新
    3. isBatchingEventUpdates=true
    4. try{
    5. //这里执行了事处理函数,比如在一次点击事件中触发setState,那么它将在这个函数执行
    6. return batchEventUpdateImpl(fn,a,b);
    7. }finally{
    8. //try里面的return 不会影响finally执行
    9. //完成一次事件,批量更新
    10. isBatchingEventUpdates=false
    11. }
    12. }

    如上分析出流程图,在React事件执行之前通过isBatchEventUpdates=true打开开关开启事件批量更新,当该事件结束,再通过isBactchEventUpdates=false;关闭开关,然后在scheduleUpdateOnFiber中根据开关来确定是否进行批量更新

    举个例子,如下组件中这么写:

    1. import React, { Component } from 'react';
    2. export default class Test extends Component {
    3. state={number:0}
    4. handleClick=()=>{
    5. this.setState({number:this.state.number+1},()=>{
    6. console.log('callback1',this.state.number)
    7. })
    8. console.log(this.state.number)
    9. this.setState({number:this.state.number+1},()=>{
    10. console.log('callback2',this.state.number)
    11. })
    12. console.log(this.state.number)
    13. this.setState({number:this.state.number+1},()=>{
    14. console.log('callback3',this.state.number)
    15. })
    16. console.log(this.state.number)
    17. }
    18. render() {
    19. return (
    20. <div>
    21. {this.state.number}
    22. <button onClick={this.handleClick}>number++button>
    23. div>
    24. );
    25. }
    26. }

    点击打印结果:0,0,0 callback1 1 ,callback2 1,callback3 1

    如上代码,在整个React上下文执行栈中会变成这样:

    那么,为什么异步操作里面的批量更新规则会被打破呢?比如用promise或者setTime在handleClick中这么写:

    1. handleClick=()=>{
    2. setTimeout(()=>{
    3. this.setState({number:this.state.number+1},()=>{
    4. console.log('callback1',this.state.number)
    5. })
    6. console.log(this.state.number)
    7. this.setState({number:this.state.number+1},()=>{
    8. console.log('callback2',this.state.number)
    9. })
    10. console.log(this.state.number)
    11. this.setState({number:this.state.number+1},()=>{
    12. console.log('callback3',this.state.number)
    13. })
    14. console.log(this.state.number)
    15. })
    16. }

     打印:callback1 1,1,callback2 2,2,callback3 3 ,3

    那么整个React上下文执行栈就会变成如图这样

     所以批量更新规则被打破。

    那么,如果在如上异步环境下,继续开启批量更新模式呢?

    React-Dom提供了批量更新方法unstable_batchChedUpdates,可以去手动批量更新,可以将上述setTimeout里面的内容作如下修改:

    1. import ReactDom from 'react-dom';
    2. const {unstable_batchedUpdates}=ReactDom
    1. setTimeout(()=>{
    2. unstable_batchedUpdates(()=>{
    3. this.setState({number:this.state.number+1},()=>{
    4. console.log('callback1',this.state.number)
    5. })
    6. console.log(this.state.number)
    7. this.setState({number:this.state.number+1},()=>{
    8. console.log('callback2',this.state.number)
    9. })
    10. console.log(this.state.number)
    11. this.setState({number:this.state.number+1},()=>{
    12. console.log('callback3',this.state.number)
    13. })
    14. console.log(this.state.number)
    15. })
    16. })

    点击打印结果:0,0,0 callback1 1 ,callback2 1,callback3 1

    在实际工作中,unstable_batchChedUpdates可以用于Ajax数据交互之后,合并多次setState,或者是多次useState。原因很简单,所有的数据交互都是在异步环境下,如果没有批量更新处理,一次数据交互多次改变state会促使视图多次渲染。

    那么如何提升更新优先级呢?

    React-dom提供了flushSync,flushSync可以将回调函数中的更新任务,放在一个比较高的优先级中。React设定了很多不同优先级的更新任务,如果一次更新任务在flushSync回调内部,那么将获得一个比较高优先级的更新。

    2 函数组件中的state

    React-hooks发布之后,useState可以使函数组件像类组件一样拥有state,也就是说名函数组件可以通过useState来改变UI视图。那么useState到底应该如何使用,底层优势怎么运行的呢?

    useState的用法

    const [state,setState] = useState(initData)
    • state,目的是提供给UI,作为渲染视图的数据源
    • setState改变state的函数,可以理解为推动函数组件渲染的渲染函数
    • initData有两种情况,第一种情况是非函数,将作为state的初始化的值,第二种情况是函数,函数返回值作为作为useState初始化的值

    initData为函数的情况:

    const [number,setNumber]= React.useState(0)

    initData为函数的情况:

    1. const [number,setNumber]=React.useState(()=>{
    2. //props中的a=1 state为0-1随机数
    3. //props中a=2 state为1-10的随机数
    4. //否则 state为1-100的随机数
    5. if(props.a===1) return Math.random()
    6. if(props.a===2) return Math.ceil(Math.random()*10)
    7. return Math.ceil(Math.random()*100)
    8. })

    对于setState参数,也有两种情况:

    • 第一种是非函数情况,此时将作为新的值,赋予state,作为下一次渲染使用;
    • 第二种是函数的情况,如果setState的参数是一个函数,这里可以称它为reducer,reducer参数,是上一次返回最新的state,返回值作为新的state

    setState参数是一个非函数的情况:

    1. const [number,setNumber]= React.useState(0)
    2. const handleClick=()=>{
    3. setNumber(1)
    4. setNumber(2)
    5. setNumber(3)
    6. }

    setState参数是一个函数的情况:

    1. const [number,setNumber]= React.useState(0)
    2. const handleClick=()=>{
    3. setNumber((state)=>{
    4. return state+1//0+1=1
    5. })
    6. setNumber(8)//8
    7. setNumber((state)=>{
    8. return state+1//8+1=9
    9. })
    10. }

    如何监听state的变化?

    类组件中的setState中,有第二个参数callback或是生命周期函数componentDidUpdate可以检测监听到state改变或是组件更新。

    那么在函数组件中,如何监听state变化呢?这个时候就需要useEffect出场了,通常可以把state作为依赖项传入useEffect第二个参数deps,但是注意useEffect初始化是会默认执行一遍

    1. import React,{useEffect, useState} from 'react'
    2. import ReactDom from 'react-dom'
    3. export default function Test() {
    4. const [number,setNumber]= React.useState(0)
    5. const handleClick=()=>{
    6. ReactDom.flushSync(()=>{
    7. setNumber(2)
    8. })
    9. setNumber(1)
    10. setTimeout(()=>{
    11. setNumber(3)
    12. })
    13. }
    14. useEffect(()=>{
    15. console.log('变化',number)
    16. },[number])
    17. console.log(number)
    18. return (
    19. <button onClick={handleClick}>text1button>
    20. )
    21. }

     执行结果:

     setState(dispatch)更新特点

    上述讲到的批量更新和flushSync,在函数组件中,dispatch更新效果和类组件是一样的,但是useState有一点值得注意,就是当帝爱用改变state的函数dispatch,在本次函数执行上下文中,是获取不到state的值的,举例如下:

    1. const [number,setNumber]= React.useState(0)
    2. const handleClick=()=>{
    3. ReactDom.flushSync(()=>{
    4. setNumber(2)
    5. console.log(number)
    6. })
    7. setNumber(1)
    8. console.log(number)
    9. setNumber(()=>{
    10. setNumber(3)
    11. console.log(number)
    12. })
    13. }

    结果:0 0 0

    原因很简单,函数组件更新就是函数的执行,在函数一次执行过程中,函数内部所有变量重新生命,所以改变的state,只有在下一次函数组件执行时才会更新,所以在如同上一个函数执行上下文中,number一直为0,无论怎么打印,都拿不到最新的state。

    useState的注意事项

    在使用useState的dispatchAction更新state的时候,记得不要传入相同的state,这样会使视图不更新,比如下面:

    1. const [state,dispatchState]= React.useState({
    2. name:'aline'
    3. })
    4. const handleClick=()=>{
    5. state.name='aline'
    6. dispatchState(state)//直接改变state,在内存中执行的地址没有变
    7. }

    上述例子为什么没有更新呢?是因为在useState的dispatchAction处理逻辑中,会浅比较state两次,发现state相同,不会开启更新调度任务。其中demo中两次state指向了相同的内存空间,所以默认为state相等,就不会发生视图更新了

    解决问题:把上述的dispatchState改成dispatch({...state})根本解决了问题,浅拷贝了对象,重新开启了内存空间。

    总结

    类组件中的setState和函数组件中的useState有什么异同?

    相同点

    • 首先从原理角度出发,setState和useState更新视图,底层都调用了scheduleUpdateOnFiber方法,而且时间驱动情况下都有批量更新规则

    不同点

    • 再不是pureComponent组件模式下,setState不会浅比较两次的state的值,只有调用setState,在没有其他优化手段的前提下,会执行更新,但是useState中的dispatchAction会默认比较两次state是否相同,然后决定是否更新组件
    • setState有专门监听state变化的回调函数callback,着这个回调函数中可以获取到最新的值,而在函数组件中,只能通过useEffect来执行state变化引起副作用。
    • setState在顶层处理state的逻辑主要是和旧state进行合并操作,而useState则是替换,及重新赋值

  • 相关阅读:
    14.Oracle中的事务
    数据中台、BI业务访谈(四)—— 十个问题看本质
    RSA公钥密码算法和Diffie-Hellman密钥交换
    基于C#的公交充值管理系统的设计与实现
    puppeteer在mac和linux上表现不一致的问题记录
    8位ADC模板实例
    java实现给图片添加水印(文字水印或图片水印)
    MyEclipse数据库工具使用教程:使用 SQL
    uniApp笔记
    Unity3D学习笔记6——GPU实例化(1)
  • 原文地址:https://blog.csdn.net/weixin_46872121/article/details/127826246