react进阶第三讲——state

React 有两个重要阶段,render 阶段和 commit 阶段,React 在调和( render )阶段会深度遍历React fiber树,目的就是发现不同(diff),不同的地方就是接下来需要更新的地方,对于变化的组件,就会执行render函数。在一次调和过程完毕之后,就到了commit 阶段,commit 阶段会创建修改真实的 DOM 节点。

setState

/* 第一个参数为function类型 */
this.setState((state,props)=>{
    return { number:1 } 
})
/* 第一个参数为object类型 */
this.setState({ number:1 },()=>{
    console.log(this.state.number) //获取最新的number
})

假如一次事件中触发一次如上 setState ,在 React 底层主要做了那些事呢?

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

更新流程如下图:
react进阶第三讲——state

setState原理

在第二章component中,讲到类组件初始化过程中绑定了负责更新的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);
}

Q:setState到底是同步的还是异步的

从React底层代码来讲,这个问题涉及到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。

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

react进阶第三讲——state

为什么异步操作里面的批量更新规则会被打破呢

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 上下文执行栈中就会变成如下图这样:
react进阶第三讲——state

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

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

在实际工作中,unstable_batchedUpdates 可以用于 Ajax 数据交互之后,合并多次 setState,或者是多次 useState 。

提升更新优先级

React-dom 提供了 flushSync,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

flushSync补充说明:flushSync 在同步条件下,会合并之前的 setState | useState,可以理解成,如果发现了 flushSync ,就会先执行更新,如果之前有未更新的 setState | useState ,就会一起合并了,所以就解释了如上,2 和 3 被批量更新到 3 ,所以 3 先被打印。

React 同一级别更新优先级关系是:

flushSync 中的 setState > 正常执行上下文中 setState > setTimeout ,Promise 中的 setState。

useState

基本用法

 [ ①state , ②dispatch ] = useState(③initData)

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

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

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

// disoatch 参数是一个函数
const [ number , setNumbsr ] = React.useState(0)
const handleClick=()=>{
   setNumber((state)=> state + 1)  // state - > 0 + 1 = 1
   setNumber(8)  // state - > 8
   setNumber((state)=> state + 1)  // state - > 8 + 1 = 9
}

注意:当调用改变 state 的函数dispatch,在本次函数执行上下文中,是获取不到最新的 state 值的。因为函数组件更新就是函数的执行,在函数一次执行过程中,函数内部所有变量重新声明,所以改变的 state ,只有在下一次函数组件执行时才会被更新。

useState原理

见后面章节

setState和useState的区别

  1. 相同点
    从底层来看,setState和 useState 都调用了 scheduleUpdateOnFiber 方法,而且事件驱动情况下都有批量更新规则。
  2. 不同点
  • setState 的 callback函数可以获取最新state;但是在函数组件中,只能通过 useEffect 来执行 state 变化引起的副作用。
  • setState 在底层处理逻辑上主要是和老 state 进行合并处理,而 useState 更倾向于重新赋值。

本文学习自掘金小册《React进阶实践指南》

上一篇:React setState 既是同步也是异步


下一篇:探讨:异步与同步的执行差异(结合async await 与 setState)