React State

135 阅读6分钟

前言

state,可以理解为组件的内存,React项目中UI的改变来源于state的改变,在目前React的多种模式下,state的更新会有不同的表现,本文主要是让大家了解React更新流程,以及归纳总结了类组件setState和函数组件useState的诸多细节问题

类组件中的state

类组件我们使用setState方法来合并更新state,基本用法如下:

setState(obj, callback)
  • 第一个参数:当 obj 为一个对象,则为即将合并的 state;如果 obj 是一个函数,那么当前组件的 state 和 props 将作为参数,返回值用于合并新的 state。

  • 第二个参数 callback:callback 为一个函数,函数执行上下文中可以获取当前 setState 更新后的最新 state 的值,可以作为依赖 state 变化的副作用函数,可以用来做一些基于 DOM 的操作。

/* 第一个参数为function类型 */ 
this.setState((state,props)=>{ 
    return { number:1 } 
}) 

/* 第一个参数为object类型 */ 
this.setState(
    { 
        number:1 
    },
    ()=>{ 
        console.log(this.state.number) //获取最新的number 
    }
)
  • 首先,setState会产生当前更新的优先级(老版本用expirationTime ,新版本用lane)。

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

  • 接下来到commit阶段,commit阶段,替换真实DOM,完成此次更新流程。

  • 此时仍然在commit阶段,会执行setState中callback函数,如上的()=>{ console.log(this.state.number) },到此为止完成了一次setState全过程。

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

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

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

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

  • pureComponent 可以对 state 和 props 进行浅比较,如果没有发生变化,那么组件不更新

  • shouldComponentUpdate 生命周期可以通过判断前后 state 变化来决定组件需不需要更新,需要更新返回true,否则返回false

setState原理揭秘

setState底层实际上是调用了类组件在初始化时绑定的了负责更新的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); 
}

batchUpdate的更新时机

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

/* 在`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; 
    } 
 }
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 中根据这个开关来确定是否进行批量更新。

而在异步回调函数中,这个规则会被打破。但可以通过unstable_batchedUpdates方法,手动指定要批量更新的setState

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) 
     })
})

函数组件中的state

React-hooks 正式发布以后, useState 可以使函数组件像类组件一样拥有 state,也就说明函数组件可以通过 useState 改变 UI 视图。那么 useState 到底应该如何使用,底层又是怎么运作的呢,首先一起看一下useState。

useState用法

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

对于dispatch也有两种情况:

  • 第一种非函数情况,此时将作为新的值,赋予给state,作为下一次渲染使用;

  • 第二种是函数的情况,如果dispatch的参数为一个函数,这里可以称它为reducer,reducer参数,是上一次返回最新的state,返回值作为新的state。

监听state变化

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

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

useState注意事项

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

export default function Index(){
    const [state, dispatchState] = useState({ name:'alien' });
    const handleClick = ()=>{ 
        // 点击按钮,视图没有更新。 
        state.name = 'Alien' 
        dispatchState(state) // 直接改变 `state`,在内存中指向的地址相同。 
    } 
    return <div> 
        <span>{ state.name }</span> 
        <button onClick={handleClick}>changeName++</button>
    </div>
}

setState和useState的异同

相同点

  • setState和 useState 更新视图,底层都调用了 scheduleUpdateOnFiber 方法,而且事件驱动情况下都有批量更新规则。

不同点

  • 在不是 pureComponent 组件模式下, setState 不会浅比较两次 state 的值,只要调用 setState,在没有其他优化手段的前提下,就会执行更新。但是 useState 中的 dispatchAction 会默认比较两次 state 是否相同,然后决定是否更新组件。

  • setState 有专门监听 state 变化的回调函数 callback,可以获取最新state;但是在函数组件中,只能通过 useEffect 来执行 state 变化引起的副作用。

  • setState 在底层处理逻辑上主要是和老 state 进行合并处理,而 useState 更倾向于重新赋值。