State (Legacy 模式)
State (Legacy 模式)
legacy模式blocking模式concurrent模式
类组件中的 state
setState
setState() 将对组件 state 的更改排入队列,并通知 React 需要使用更新后的 state 重新渲染此组件及其子组件。setState() 并不总是立即更新组件,它会批量推迟更新。
setState(updater[, callback])
updater- 当 
updater是一个函数时,会接受state和props作为参数,其返回值会与state进行浅合并。 - 当 
updater是一个对象时,会将传入的对象浅层合并到新的state中。 
- 当 
 callback: 回调函数。在setState完成合并并重新渲染组件后执行。可以获取当前setState更新后的最新state的值,可以作为依赖state变化的副作用函数,可以用来做一些基于 DOM 的操作。
// setState 第一个参数为【函数】
this.setState(
  (state, props) => {
    return { updateStateKey: updateStateValue }
  },
  () => {
    console.log('更新 state 之后回调,调用 this.state.xxx 可获取最新值')
  }
)
// setState 第一个参数为【对象】
this.setState(
  {
    updateStateKey: updateStateValue,
  },
  () => {
    console.log('更新 state key 之后回调,调用 this.state.xxx 可获取最新值')
  }
)flushSync
在罕见的情况下,如果需要强制 DOM 更新同步应用,可以使用 flushSync()。
flushSync(callback) : 强制 React 同步刷新提供的回调函数中的任何更新。这确保了 DOM 会被立即更新。
flushSync() 会对性能产生很大影响,尽量少用。
export default class SetStateUpdate extends React.Component {
  constructor(props) {
    super(props)
  }
  state = {
    number: 0,
  }
  handleFlushSyncClick = () => {
    setTimeout(() => {
      this.setState({ number: 1 })
    })
    this.setState({ number: 2 })
    ReactDOM.flushSync(() => {
      this.setState({ number: 3 })
    })
    this.setState({ number: 4 })
  }
  render() {
    console.log('render : ', this.state.number)
    return (
      <div>
        <div>{this.state.number}</div>
        <button onClick={this.handleFlushSyncClick}>flushSync number++</button>
      </div>
    )
  }
}
// 触发 handleFlushSyncClick 点击事件打印结果如下:
// render : 3
// render : 4
// render : 1以上 Demo 打印结果解析,在 handleFlushSyncClick 中:
首先,
ReactDOM.flushSync(() => { this.setState({ number: 3 }) })设定了一个高优先级的更新。更新number为2和3,会被批量更新到3,则,render : 3会被优先打印flushSync在同步条件下,会合并之前的setState或者useState。可以理解成,如果发现了flushSync,就会先执行更新,如果之前有未更新的setState或者useState,就会一起合并了接着,更新
number为4,则,render : 4会被优先打印最后,更新
setTimeout中的number,则,render : 1会被优先打印
React 同一级别更新优先级关系是:
flushSync 中的 setState > 正常执行上下文中 setState > setTimeout ,Promise 中的 setState。
限制 state 更新视图
对于类组件,可通过如下方式限制 state 的更新
React.PureComponent
React.PureComponent 与 React.Component 的区别在于 React.PureComponent 中以浅层对比 prop 和 state 的方式实现了 shouldComponentUpdate() 函数。
React.PureComponent中的shouldComponentUpdate()仅作对象的浅层比较。如果对象中包含复杂的数据结构,则有可能因为无法检查深层的差别,产生错误的比对结果。在深层数据结构发生变化时,可以调用
forceUpdate()来确保组件被正确地更新。也可以考虑使用 immutable 对象加速嵌套数据的比较。React.PureComponent中的shouldComponentUpdate()将跳过所有子组件树的prop更新。
shouldComponentUpdate
当 props 或 state 发生变化时,shouldComponentUpdate(nextProps, nextState) 会在渲染执行之前被调用。根据 shouldComponentUpdate() 的返回值,判断 React 组件的输出是否受当前 state 或 props 更改的影响。
- 返回为 
true: 组件会被重新渲染,默认行为。 - 返回为 
false: 组件不会被重新渲染,不会调用UNSAFE_componentWillUpdate(),render()和componentDidUpdate()。返回false并不会阻止子组件在state更改时重新渲染。 
此方法仅作为性能优化的方式而存在。不要企图依靠此方法来“阻止”渲染,因为这可能会产生 bug。应该考虑使用内置的 PureComponent 组件,而不是手动编写 shouldComponentUpdate()。React.PureComponent 会对 props 和 state 进行浅层比较,并减少了跳过必要更新的可能性。
后续版本,React 可能会将 shouldComponentUpdate() 视为提示而不是严格的指令,并且,当返回 false 时,仍可能导致组件重新渲染。
setState 更新
类组件 setState 示例
import React from 'react'
export default class SetStateUpdate extends React.Component {
  constructor(props) {
    super(props)
  }
  state = {
    number: 0,
  }
  handleAddEventListenerClick = () => {
    this.setState({ number: this.state.number + 1 }, () => {
      console.log(
        '【handleAddEventListenerClick】 setState callback : ',
        this.state.number
      )
    })
    console.log(
      '【handleAddEventListenerClick】 setState : ',
      this.state.number
    )
  }
  componentDidMount() {
    document
      .getElementById('addEventListenerClickBtn')
      .addEventListener('click', this.handleAddEventListenerClick, false)
    this.setState({ number: this.state.number + 1 }, () => {
      console.log(
        '【componentDidMount】 setState callback : ',
        this.state.number
      )
    })
    console.log('【componentDidMount】 setState : ', this.state.number)
  }
  handleClick = () => {
    this.setState({ number: this.state.number + 1 }, () => {
      console.log('【handleClick】 setState callback1 : ', this.state.number)
    })
    console.log('【handleClick】 setState : ', this.state.number)
    this.setState({ number: this.state.number + 1 }, () => {
      console.log('【handleClick】 setState callback2 : ', this.state.number)
    })
    console.log('【handleClick】 setState : ', this.state.number)
  }
  handleSetTimeoutClick = () => {
    setTimeout(() => {
      this.setState({ number: this.state.number + 1 }, () => {
        console.log(
          '【handleClick setTimeout】 setState callback1 : ',
          this.state.number
        )
      })
      console.log('【handleClick setTimeout】 setState : ', this.state.number)
      this.setState({ number: this.state.number + 1 }, () => {
        console.log(
          '【handleClick setTimeout】 setState callback2 : ',
          this.state.number
        )
      })
      console.log('【handleClick setTimeout】 setState : ', this.state.number)
    })
  }
  render() {
    return (
      <div>
        <div>{this.state.number}</div>
        <button id="addEventListenerClickBtn">addEventListener click</button>
        <button onClick={this.handleClick}> number++ </button>
        <button onClick={this.handleSetTimeoutClick}>
          setTimeout number++
        </button>
      </div>
    )
  }
}
// 【React 16.8.0】输出结果,number 初始值为 0
// ========== 【componentDidMount】 ==========
// 【componentDidMount】 setState : 0
// 【componentDidMount】 setState callback : 1
// ========== 【handleAddEventListenerClick】 ==========
// 【handleAddEventListenerClick】 setState callback : 2
// 【handleAddEventListenerClick】 setState : 2
// ========== 【handleClick】 ==========
// 【handleClick】 setState : 2
// 【handleClick】 setState : 2
// 【handleClick】 setState : 2
// 【handleClick】 setState callback1 : 3
// 【handleClick】 setState callback2 : 3
// 【handleClick】 setState callback3 : 3
// ========== 【handleClick setTimeout】 ==========
// 【handleClick setTimeout】 setState callback1 : 4
// 【handleClick setTimeout】 setState : 4
// 【handleClick setTimeout】 setState callback2 : 5
// 【handleClick setTimeout】 setState : 5
// 【handleClick setTimeout】 setState callback3 : 6
// 【handleClick setTimeout】 setState : 6
// 【React 17.0.0】输出结果,number 初始值为 0
// ========== 【componentDidMount】 ==========
// 【componentDidMount】 setState : 0
// 【componentDidMount】 setState callback : 1
// ========== 【handleAddEventListenerClick】 ==========
// 【handleAddEventListenerClick】 setState callback : 2
// 【handleAddEventListenerClick】 setState : 2
// ========== 【handleClick】 ==========
// 【handleClick】 setState : 2
// 【handleClick】 setState : 2
// 【handleClick】 setState : 2
// 【handleClick】 setState callback1 : 3
// 【handleClick】 setState callback2 : 3
// 【handleClick】 setState callback3 : 3
// ========== 【handleClick setTimeout】 ==========
// 【handleClick setTimeout】 setState callback1 : 4
// 【handleClick setTimeout】 setState : 4
// 【handleClick setTimeout】 setState callback2 : 5
// 【handleClick setTimeout】 setState : 5
// 【handleClick setTimeout】 setState callback3 : 6
// 【handleClick setTimeout】 setState : 6
// 【React 18.2.0】输出结果,number 初始值为 0
// ========== 【componentDidMount】 ==========
// 【componentDidMount】 setState : 0
// 【componentDidMount】 setState callback : 1
// ========== 【handleAddEventListenerClick】 ==========
// 【handleAddEventListenerClick】 setState : 1
// 【handleAddEventListenerClick】 setState callback : 2
// ========== 【handleClick】 ==========
// 【handleClick】 setState : 2
// 【handleClick】 setState : 2
// 【handleClick】 setState : 2
// 【handleClick】 setState callback1 : 3
// 【handleClick】 setState callback2 : 3
// 【handleClick】 setState callback3 : 3
// ========== 【handleClick setTimeout】 ==========
// 【handleClick setTimeout】 setState : 3
// 【handleClick setTimeout】 setState : 3
// 【handleClick setTimeout】 setState : 3
// 【handleClick setTimeout】 setState callback1 : 4
// 【handleClick setTimeout】 setState callback2 : 4
// 【handleClick setTimeout】 setState callback3 : 4React 16.x / 17.x
setState在合成事件和钩子函数中是 "异步" 的,在原生事件和setTimeout中是 同步 的。setState的 "异步" 并不是内部由异步代码实现,其本身执行过程和代码是同步的。合成事件和钩子函数的调用顺序在更新之前,导致合成事件和钩子函数无法立刻获取到更新后的值。可以通过setState(updater[, callback])中的callback回调函数获取到更新后的结果。setState的 批量更新 优化是建议在 "异步" (合成事件、钩子函数)之上的,在原生事件和setTimeout中不会批量更新。在 "异步" 中,如果对同一个值进行多次setState,setState的批量更新策略会对其进行覆盖,取最后一次的执行,如果是同时setState多个不同的值,在更新时会对其进行合并批量更新。
注:合成事件是 React 在原生的 DOM 事件上的一层封装,称为 SyntheticEvent(合成事件)。它是浏览器的原生事件的跨浏览器包装器。除兼容所有浏览器外,它还拥有和浏览器原生事件相同的接口,包括 stopPropagation() 和 preventDefault()。
React 18.x
- 使用 
ReactDOM.createRoot,则 React 会在所有事件中进行自动批量更新(Automatic Batching) 。而如果 - 使用 
ReactDOM.render,则会依旧维持之前版本的表现。 
触发 setState 的更新流程
触发一次 setState ,更新流程如下:
- 触发 
setState - 计算优先级。
setState会产生当前更新的优先级(老版本使用expirationTime,新版本使用lane) - 更新调度,调和 
fiber树。React 会从 fiber Root 根部fiber向下调和子节点,调和阶段将对比发生更新的地方,更新对比expirationTime,找到发生更新的组件。 - 合并 
state,执行render。合并state,然后触发render函数,得到新的 UI 视图层,完成render阶段。 commit阶段,替换真实 DOM,完成更新流程。setState回调函数执行callback。此时仍然在commit阶段,会执行setState中callback函数。
setState 实现原理
在类组件中调用 this.setState ,本质上是调用了挂载在 Component 类组件构造函数原型链上的 setState 方法,进而 updater 对象上的 enqueueSetState 方法。
enqueueSetState 方法,创建一个 update,然后放入当前 fiber 对象的待更新队列中,最后开启调度更新。
【enqueueSetState】方法
// packages\react-reconciler\src\ReactFiberClassComponent.old.js
const classComponentUpdater = {
  isMounted,
  enqueueSetState(inst, payload, callback) {
    const fiber = getInstance(inst)
    const eventTime = requestEventTime() // 获取更新触发的时间
    const lane = requestUpdateLane(fiber) // 获取任务优先级
    const update = createUpdate(eventTime, lane) // 创建更新任务
    update.payload = payload
    if (callback !== undefined && callback !== null) {
      update.callback = callback
    }
    const root = enqueueUpdate(fiber, update, lane) // 将任务推入更新队列
    if (root !== null) {
      scheduleUpdateOnFiber(root, fiber, lane, eventTime) // schedule 进行调度
      entangleTransitions(root, fiber, lane)
    }
    if (enableSchedulingProfiler) {
      markStateUpdateScheduled(fiber, lane)
    }
  },
  enqueueReplaceState(inst, payload, callback) {
    // ...
  },
  enqueueForceUpdate(inst, callback) {
    // ...
  },
}函数组件中的 state
useState
const [state, setState] = useState(initialState);
useState 返回一个 state,以及更新 state 的函数。
initialState: 初始值。只会在组件的初始渲染中起作用,后续渲染时会被忽略。initialState为非函数情况下,将作为state初始化的值const [number, setNumber] = useState(0)initialState为函数的情况下,函数返回值作为state初始化的值const [number, setNumber] = useState(() => { /* 在 props 中,当 a = 1, state 为 0-1 随机数;当 a = 2, state 为 1-10随机数;否则,state 为 1 - 100 随机数 */ if (props.a === 1) return Math.random() if (props.a === 2) return Math.ceil(Math.random() * 10) return Math.ceil(Math.random() * 100) })
state: 返回的状态。在初始渲染期间,返回的状态 (state) 与传入的第一个参数 (initialState) 值相同。setState()函数 : 用于更新state。它接收一个新的state值并将组件的一次重新渲染加入队列。setState(value)接受参数为非函数的情况下,设置的state将作为新值,赋值给state,作为下一次渲染使用。setState((prevValue) => value)接受参数为函数的情况下,该函数将接收先前的state,并返回一个更新后的值。
const [number, setNumber] = useState(0) handleClick = () => { setNumber(1) setNumber(number + 1) setNumber(prevValue => prevValue + 1) }
在 useState() 的 setState() 函数处理逻辑中,会浅比较两次 state ,如果 state 相同,则不会开启更新调度任务。
监听 state 变化
类组件 setState 中,可通过 setState(updater[, callback]) 中 callback 回调函数或者生命周期 componentDidUpdate 检测监听到 state 的改变或者组件更新。
对于监听 useState() Hooks 的 state 变化,可以通过 useEffect() Hooks ,将 state 作为依赖项传入 useEffect() 的第二个参数 deps。useEffect() 在初始化时,会默认执行一次。
import React, { useState, useEffect } from 'react'
import { flushSync } from 'react-dom'
const UseStateUpdate = function () {
  const [number, setNumber] = useState(0)
  /* 监听 number 变化 */
  useEffect(() => {
    console.log('【useEffect】 number : ' + number)
  }, [number])
  const handleClick = () => {
    /** 高优先级更新 **/
    flushSync(() => {
      setNumber(2)
      console.log('【handleClick】 flushSync', number)
    })
    /* 批量更新 */
    setNumber(1)
    console.log('【handleClick】 setNumber', number)
    /* 滞后更新 ,批量更新规则被打破 */
    setTimeout(() => {
      setNumber(3)
      console.log('【handleClick】 setTimeout setNumber', number)
    })
  }
  console.log('【render】 number', number)
  return (
    <div>
      <div> {number}</div>
      <button onClick={handleClick}> number++ </button>
    </div>
  )
}
export default UseStateUpdate
// 【render】 number : 0
// 【useEffect】 number : 0
// ========== 【触发 handleClick】 ==========
// 【handleClick】 flushSync : 0
// 【render】 number : 2
// 【useEffect】 number : 2
// 【handleClick】 setNumber : 0
// 【render】 number : 1
// 【useEffect】 number : 1
// 【handleClick】 setTimeout setNumber : 0
// 【render】 number : 3
// 【useEffect】 number : 3在 Demo 中, 输出 【handleClick】 flushSync : 0 、 【handleClick】 setNumber : 0 、 【handleClick】 setTimeout setNumber : 0 是因为函数组件更新就是函数的执行,在函数一次执行过程中,函数内部所有变量重新声明,所以改变的 state ,只有在下一次函数组件执行时才会被更新。所以在同一个函数执行上下文中,number 一直为 0,都拿不到最新的 state 。
useState 与 setState 的异同
相同点:从原理角度出发,
setState和useState更新视图,底层都调用了scheduleUpdateOnFiber方法,而且事件驱动情况下都有批量更新规则。不同点:
- 在非 
pureComponent组件模式下,setState不会浅比较两次state的值,只要调用setState,在没有其他优化手段的前提下,就会执行更新。但是useState中的setState会默认浅比较两次state是否相同,然后决定是否更新组件。 setState有专门监听state变化的回调函数callback,可以获取最新state;但是在函数组件中,只能通过useEffect来执行state变化引起的副作用。setState在底层处理逻辑上主要是和老state进行合并处理,而useState更倾向于重新赋值。
- 在非 
 
