可视化大作业引出useState的坑

131 阅读5分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第1天,点击查看活动详情

你好,我是南一。这是我在准备面试八股文的笔记,如果有发现错误或者可完善的地方,还请指正,万分感谢🌹

数据可视化大作业,做了两天,用了React、Echart。过程中遇到很多关于React Hook的问题,本文将一一记录

一、useState的浅比较

useState 返回的更新 state 的 dispatch 函数,会浅比较两次的state,发现相同则不会开启更新调度任务demo 中两次 state 指向了相同的内存空间,所以默认为 state 相等,就不会发生视图更新了。举个🌰

import { useEffect, useState } from "react"

export default function Index() {
  const [option, setOption] = useState({
    name: 'Nanyi',
    age: 18,
    sex: 'man',
    data: []
  })

  const onClick = function () {
    new Promise((resolve) => {
      resolve([1, 2, 3, 4, 5])
    }).then((res) => {
      option.data = res
      setOption(option)
    })
  }

  useEffect(() => {
    console.log(option);
  }, [option])

  return <div>
    <button onClick={onClick}>点击</button>
  </div>
}

在此Demo中,首次渲染会触发useEffect,打印出一个对象,当你点击按钮之后,没有任何反应,这是因为要更新的option跟原来的option是指向同一个对象,在 useState 的 dispatchAction 处理逻辑中,会进行浅比较,发现一样,就不会开启更新调度任务,useEffect依赖于option,自然也就没有调用回调函数

解决办法:setOption({...option})将对象进行一次浅拷贝即可。

通过官网可知,此处浅比较用的是Object.is比较算法

useState 跟 setState 的异同

相同点:

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

不同点:

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

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

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

二、useState函数式更新

先看例子,我想要点击按钮之后,页面信息更新为Mike,并且发请求获取数据注入oprion中

export default function Index() {
  const [key, setKey] = useState(0)
  const [option, setOption] = useState({ name: 'Nanyi', age: 18, data: [] })

  const onClick = function () {
    setKey(key + 1)
    // 模拟发请求
    new Promise((resolve) => {
      resolve([1, 2, 3, 4, 5])
    }).then((res) => {
      option.data = res
      setOption({ ...option })
    })
  }

  useEffect(() => {
    // 首次渲染不要更新
    key && setOption({ name: 'Mike', age: 21, data: [] })
  }, [key])

  useEffect(() => {
    console.log(option);
  }, [option])

  return <div>
    名字:{option.name}<br />
    <button onClick={onClick}>点击</button>
  </div>
}

实际效果,页面信息仍为Nanyi,且请求来的数据也注到Nanyi的对象中。

原因: 实际上setOption({ name: 'Mike', age: 21, data: [] })已经执行了,但是setOption({ ...option })获取到的状态不是最新的。

解决方法:setOption(preOption => { ...preOption }) 用函数式更新,可以取到最新一次更新的状态。

三、状态更新批处理

React可以将多个状态更新分组到单个重新渲染中,以提高性能。通常,这会提高性能,不会影响应用程序的行为。

在React 18之前,只对React事件处理程序内部的更新进行批处理。从React 18开始,默认情况下为所有更新启用批处理。 请注意,React确保来自多个不同用户发起的事件的更新(例如,单击两次按钮)始终是单独处理的,不会进行批处理。这可以防止逻辑错误。

在罕见的情况下,您需要强制同步应用DOM更新,您可以将其包装在flushSync中。然而,这可能会影响性能,所以只在需要时才这样做。

  const handleClick = function () {
    // 批量更新
    setNum(1)
    setNum(4)
    // 高优先级更新
    ReactDom.flushSync(() => {
      setNum(2)
    })
    setNum(5)
    setNum(6)
    // 滞后更新 批量更新规则被打破
    setTimeout(() => {
      setNum(3)
      setNum(7)
      setNum(8)
    }, 0)
  }

  useEffect(() => {
    console.log(num);
  })

结果输出 2,6,8

四、setState 是同步还是异步?

React18之前,我们会说在在合成事件和钩子函数中是“异步”的,在原生事件和setTimeout 中都是同步的。所谓的异步,就是setState执行后能不能立即获取到最新的数据。批量更新优化也是建立在“异步”(合成事件、钩子函数)之上的,在原生事件和setTimeout 中不会批量更新。

但是React18就都是"异步"的了

class Index extends Component {
  state = {
    data: 'data'
  }

  componentDidMount() {
    this.setState({ data: 'async' })
    console.log(this.state.data); // data
    setTimeout(() => {
      this.setState({ data: 'setTimeOut' })
      console.log(this.state.data); // React18:async,React17:setTimeOut 
    }, 0)
  }

  render() {
    return (
      <div></div>
    )
  }
}

五、附加知识点

1、Object.is 算法规则

以下情况判定为相等:

  1. 都是undefined
  2. 都是null
  3. 都是truefalse
  4. 都是相同长度、相同字符、按相同顺序排列的字符串
  5. 都是相同对象(意味着都是同一个对象的值引用)
  6. 都是数字且
    • 都是 +0
    • 都是 -0
    • 都是 NaN
    • 都是同一个值,非零且都不是 NaN

Object.is 与 === 、 == 有什么不同:

  • +0 === -0 为true
  • NaN === NaN 为false
  • Object.is 不会进行类型隐式转换

2、JS 隐式转换

"3" - 2 // 输出:1
"3" + 2 // 输出:"32"
3 + "2" // 输出:"32"
"3" * "2" // 输出:6
"10" / "2" // 输出:5
1 + true // 输出:2
1 + false // 输出:1
1 + undefined // 输出:NaN
3 + null // 输出:3
"3" + null // 输出:"3null"
true + null // 输出:1
true + undefined // 输出:NaN

举个🌰:"91" > "123" 输出 true,我想以数值进行比较,就得进行类型转换,可以这样 1 * "91" > "123" 输出false

原理: 将其中一个操作数用乘1转换成数字,数字与字符串进行比较运算,字符串会被转换成数字再进行比较

3、对象判空

Object.keys(obj).length