React中,setState是同步还是异步,站在前端的角度,怎么理解同步异步概念?

184 阅读4分钟

setState的同步异步问题是React中比较复杂的一个话题。让我来详细分析:

  1. setState的"异步"表现:
// 常见场景 - 看似异步
class Example extends React.Component {
  state = { count: 0 }
  
  handleClick = () => {
    this.setState({ count: this.state.count + 1 })
    console.log(this.state.count) // 打印的是更新前的值
    
    this.setState({ count: this.state.count + 1 })
    console.log(this.state.count) // 打印的是更新前的值
  }
  
  render() {
    return <button onClick={this.handleClick}>{this.state.count}</button>
  }
}
  1. setState的"同步"表现:
// 在setTimeout或原生事件中 - 表现为同步
class Example extends React.Component {
  state = { count: 0 }
  
  handleClick = () => {
    setTimeout(() => {
      this.setState({ count: this.state.count + 1 })
      console.log(this.state.count) // 立即打印更新后的值
    }, 0)
  }
}
  1. 为什么会这样?核心原因是React的批处理机制:
// React内部简化的批处理逻辑示意
let isBatchingUpdates = false
const dirtyComponents = []

function batchedUpdates(fn) {
  const previousIsBatchingUpdates = isBatchingUpdates
  isBatchingUpdates = true
  
  try {
    return fn()
  } finally {
    isBatchingUpdates = previousIsBatchingUpdates
    // 如果没有其他批处理,执行更新
    if (!isBatchingUpdates) {
      performBatchedUpdates()
    }
  }
}

function performBatchedUpdates() {
  // 处理所有待更新的组件
  dirtyComponents.forEach(component => {
    component.updateComponent()
  })
}
  1. React 18之前的批处理范围:
class Example extends React.Component {
  state = { count: 0, flag: false }
  
  // React事件中的多个setState会被批处理
  handleClick = () => {
    this.setState({ count: 1 }) // 批处理
    this.setState({ flag: true }) // 批处理
  }
  
  componentDidMount() {
    // 原生事件中的setState不会批处理
    document.addEventListener('click', () => {
      this.setState({ count: 1 }) // 直接更新
      this.setState({ flag: true }) // 直接更新
    })
    
    // Promise中的setState不会批处理
    Promise.resolve().then(() => {
      this.setState({ count: 1 }) // 直接更新
      this.setState({ flag: true }) // 直接更新
    })
  }
}
  1. React 18的自动批处理:
// React 18中,所有更新都会自动批处理
function App() {
  const [count, setCount] = useState(0)
  const [flag, setFlag] = useState(false)
  
  async function handleClick() {
    // React 18中这些都会批处理
    setCount(c => c + 1) // 批处理
    setFlag(f => !f)     // 批处理
    
    // 即使在Promise中也会批处理
    await Promise.resolve()
    setCount(c => c + 1) // 批处理
    setFlag(f => !f)     // 批处理
  }
}
  1. 如果确实需要立即获取更新后的状态:
// 1. 使用回调函数
this.setState({ count: count + 1 }, () => {
  console.log(this.state.count) // 获取更新后的值
})

// 2. 使用flushSync (React 18)
import { flushSync } from 'react-dom'

function handleClick() {
  flushSync(() => {
    setCount(c => c + 1)
  })
  // DOM已经更新
  console.log(count)
}
  1. setState的工作流程:
// 简化的setState工作流程
function setState(newState) {
  // 1. 将新状态加入更新队列
  enqueueUpdate(newState)
  
  // 2. 判断是否处于批处理模式
  if (isBatchingUpdates) {
    // 将组件标记为待更新
    dirtyComponents.push(component)
  } else {
    // 直接触发更新流程
    performUpdate()
  }
}

function performUpdate() {
  // 3. 合并所有排队的状态更新
  const nextState = processUpdateQueue()
  
  // 4. 触发重新渲染
  component.updateComponent(nextState)
}

总结:

  1. setState的"异步"本质:
  • 不是真正的异步,而是批处理机制
  • 目的是优化性能,减少不必要的重渲染
  1. React 18的改进:
  • 自动批处理范围扩大
  • 更一致的行为
  • 提供了更细粒度的控制API
  1. 实践建议:
  • 不要依赖setState的同步性
  • 需要依赖新状态时使用回调函数
  • 在React 18中尽量避免使用flushSync
  1. 原理要点:
  • 批处理队列
  • 事务机制
  • 调度系统

让我通过几个具体的例子来解释前端中的同步和异步概念。

  1. 同步代码执行:
// 同步代码按顺序执行
console.log('1') // 第一个执行
console.log('2') // 第二个执行
console.log('3') // 第三个执行

// 同步的计算任务
function sum(a, b) {
  return a + b  // 立即返回结果
}
const result = sum(1, 2)
console.log(result) // 立即得到3
  1. 异步代码执行:
// 异步代码执行顺序不确定
console.log('1')  // 第一个执行
setTimeout(() => {
  console.log('2') // 最后执行
}, 0)
console.log('3')  // 第二个执行

// 异步的网络请求
fetch('https://api.example.com/data')
  .then(res => res.json())
  .then(data => console.log(data)) // 需要等待网络响应
console.log('我先执行') // 这行会在网络请求前执行
  1. 常见的异步场景:
// 1. 定时器
setTimeout(() => {
  console.log('定时器')
}, 1000)

// 2. 事件监听
button.addEventListener('click', () => {
  console.log('按钮被点击')
})

// 3. Promise
new Promise((resolve) => {
  resolve('成功')
}).then(res => console.log(res))

// 4. async/await
async function fetchData() {
  try {
    const response = await fetch('https://api.example.com/data')
    const data = await response.json()
    console.log(data)
  } catch (error) {
    console.error('出错了:', error)
  }
}
  1. 事件循环(Event Loop)中的执行顺序:
// 宏任务和微任务的执行顺序
console.log('1') // 同步代码

setTimeout(() => {
  console.log('2') // 宏任务
}, 0)

Promise.resolve().then(() => {
  console.log('3') // 微任务
})

console.log('4') // 同步代码

// 输出顺序:1, 4, 3, 2
  1. 实际开发中的异步处理:
// 不好的写法 - 回调地狱
fetchUserData(userId, function(user) {
  fetchUserPosts(user.id, function(posts) {
    fetchPostComments(posts[0].id, function(comments) {
      console.log(comments)
    })
  })
})

// 好的写法 - Promise链
fetchUserData(userId)
  .then(user => fetchUserPosts(user.id))
  .then(posts => fetchPostComments(posts[0].id))
  .then(comments => console.log(comments))
  .catch(error => console.error(error))

// 更好的写法 - async/await
async function getUserData() {
  try {
    const user = await fetchUserData(userId)
    const posts = await fetchUserPosts(user.id)
    const comments = await fetchPostComments(posts[0].id)
    console.log(comments)
  } catch (error) {
    console.error(error)
  }
}
  1. 常见的异步陷阱:
// 循环中的异步
// 错误写法
for (let i = 0; i < 5; i++) {
  setTimeout(() => {
    console.log(i)
  }, 1000)
}

// 正确写法 - 使用async/await
async function processArray(array) {
  for (const item of array) {
    await processItem(item) // 依次处理每个项目
  }
}

// 并行处理
Promise.all(array.map(item => processItem(item)))
  1. setState的"异步":
// React setState的"异步"本质上是批处理机制
function Counter() {
  const [count, setCount] = useState(0)
  
  function handleClick() {
    // 这些更新会被批处理
    setCount(count + 1)  // 不会立即更新
    setCount(count + 1)  // 不会立即更新
    console.log(count)   // 还是原来的值
    
    // 使用函数更新方式
    setCount(prev => prev + 1) // 这样能保证累加
    setCount(prev => prev + 1) // 这样能保证累加
  }
}

理解同步异步的关键点:

  1. 代码执行顺序:

    • 同步代码立即执行
    • 异步代码需要等待某个条件或事件
  2. JavaScript的单线程特性:

    • 同一时间只能执行一段代码
    • 异步任务会被放入任务队列
  3. 事件循环机制:

    • 微任务优先于宏任务
    • 保证异步任务的执行顺序
  4. 实际应用:

    • 网络请求
    • 文件操作
    • 定时任务
    • 用户交互
  5. 性能考虑:

    • 异步可以避免阻塞主线程
    • 批处理可以优化性能