state

160 阅读6分钟

state

在学习React基础时相信肯定很多人都看到过一个问题:state到底是同步的还是异步的?

  • 在React18之前

    在组件生命周期或React合成事件中,state是异步的;

    在SetTimeout或者原生dom事件中,state是同步的;

  • 在React18之后

    默认所有操作都被放到了批处理中(异步处理)

React存在着很多模式,例如legacy,这是我们平常经常使用到的模式,除此之外还有blocking模式和concurrent模式,这里主要探讨的是legacy下的state。

类组件中的state

在类组件中setState是更新组件,重新渲染视图的主要方法,接下来就来探讨setState的用法和底层的一些原理

setState的基本用法
setState(obj,callback)
  • 第一个参数:当 obj 为一个对象,则为即将合并的 state ;如果 obj 是一个函数,那么当前组件的 state 和 props 将作为参数,返回值用于合并新的 state。
  • 第二个参数 callback :callback 为一个函数,函数执行上下文中可以获取当前 setState 更新后的最新 state 的值,可以作为依赖 state 变化的副作用函数,可以用来做一些基于 DOM 的操作。
setState底部的更新流程

(lane和expirationTime分别表示旧版本和新版本的任务调度优先级的概念)

1.setState产生当前更新的优先级(lane和expirationTime);

2.React根据fiber Root根部 fiber 向下调和子节点,调和阶段将对比发生更新的地方,更新对比 expirationTime ,找到发生更新的组件,合并 state,然后触发 render 函数,得到新的 UI 视图层,完成 render 阶段;

3.commit阶段替换真实的DOM;

4.执行setState中的回调函数。

需要注意的是:先render后再去替换DOM

类组件如何限制state更新视图
  • pureComponent 可以对 state 和 props 进行浅比较,如果没有发生变化,那么组件不更新,与函数式组件中的memo高阶组件较为相似;
  • shouldComponentUpdate 生命周期可以通过判断前后 state 变化来决定组件需不需要更新,需要更新返回true,否则返回false,这个生命周期在lifeCycle中会详细介绍。
setState的原理

​ 在上一节component中类组件的定义中提到了,类组件初始化过程中绑定了负责更新的Updater对象,对于如果调用 setState 方法,实际上是 React 底层调用 Updater 对象上的 enqueueSetState 方法。下面来看一下精简的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);
}

可以看出enqueueSetState的做法其实很简单,就是创建出一个update,将当前的fiber对象放到更新队列当中,然后开始进行上述的流程。但是可能会有一个疑惑,就是在React基础中提到的批量更新(batchUpdate)是在什么时候加进去的呢?

/* 在`legacy`模式下,所有的事件都将经过此函数同一处理 */
function dispatchEventForLegacyPluginEventSystem(){
    // handleTopLevel 事件处理函数
    batchedEventUpdates(handleTopLevel, bookKeeping);
}

重点就是这里的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;因为在这个点击事件发生时isBatchingEventUpdates=true,这时开始批量更新,三个setState会是一个异步的,所以前三个都为0,并且因为会进行state的合并,所以最终产生的是最后进来的那个对象,所以render之后再执行回调函数,number就变成了1。

示例二:

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,这个结果也不难理解,setTimeout创建了新的宏任务,会在下一轮事件循环中执行,而函数末尾将isBatchingEventUpdates设为了false,所以执行更新时isBatchingEventUpdates为false,不会进行批量更新。

如何再异步环境下,继续开启批量更新

这时候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。

既然能在异步环境下开启批量更新,那么也可能存在一个函数能够改变更新的顺序,这个函数就是flushSync

React-dom 提供了 flushSync ,flushSync 可以将回调函数中的更新任务,放在一个较高的优先级中。React 设定了很多不同优先级的更新任务。如果一次更新任务在 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

综上所述React更新的优先级是:flushSync 中的 setState > 正常执行上下文中 setState > setTimeout ,Promise 中的 setState。

函数式组件中的state

自React-hooks发布之后,函数式组件也能和类组件一样拥有state,那么useState的用法和底层是什么样的呢?

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

注:dispatch参数也有两种情况,第一种非函数情况,此时将作为新的值,赋予给 state,作为下一次渲染使用,第二种是函数的情况,如果 dispatch 的参数为一个函数,这里可以称它为reducer,reducer 参数,是上一次返回最新的 state,返回值作为新的 state。

函数式组件监听state的变化

在类组件中能够使用回调函数或者一些生命周期函数来对state的变化进行监听,而在函数式组件中并不能做到,这时候就要使用到另一个hook,叫做useEffect,这个hook用于监听一些数据的变化,并且做出对应的处理,注意useEffect默认会执行一次。

最为重要的是在调用dispatch函数时是获取不到最新的state的值的,对这个的理解类似于闭包,在初始调用时state确定为了某个值,不管在这个函数执行上下文内修改了多少次state,拿到的还是初始的state,而最终修改的state只会在下一次函数式组件更新时才能获取到。

总结

通过这章学到了:

  • setState用法详解,底层更新流程。
  • useState用法详解,注意事项。
  • 几种不同优先级的更新任务。