React中useState函数

177 阅读6分钟

什么是组件的状态及useState函数

随时间变化的数据被称为状态(state),状态是可以进行数据驱动视图的,而普通变量是不行的

状态是如何改变视图的

普通变量为什么不行?

无法重新渲染JSX,普通App组件其实是一个纯函数,只能从上往下执行,在打开浏览器后就仅执行一次,不会再通过执行来改变。

state状态为什么可行?

重新触发函数组件,并且state状态具备组件的记忆 useState可创建状态和修改状态的方法

底层原理

渲染与提交的过程(三个步骤)

步骤1:触发一次渲染(准备阶段) 组件的初次渲染,createRoot().render() 内部状态更新,触发渲染送入队列(数组)

步骤2:渲染您的组件(执行函数组件) 在进行初次渲染时,React会调用根组件 内部状态更新,会渲染对应的函数组件

步骤3:提交到DOM上(想办法更新真实的dom) 初次渲染,appendChild() DOM API 和先前的状态比对,更新差异的DOM节点

举一个简单的例子

import { useState } from 'react'
function App() {
  const [count, setCount] = useState(0)
  const handelClick = () => {
    setCount(count + 1)
  }
  return (
    <div onClick={handelClick}>
      hello App
      {count}
    </div>
  )
}
export default App

通过数组解构语法将变量count,和能够改变的变量函数setCount从useState钩子中解构出来。只有调用setCount才能重新渲染。

多状态是如何进行正确记忆的

在同一组件的每次渲染中,useState都依托于一个稳定的调用顺序

在React内部,为每个组件保存了一个数组,其中每一项都是一个state对。它维护当前state对的索引值,在渲染之 前将其设置为“0”。每次调用useState时,React都会为你提供一个state对并增加索引值。 注意:不能在逻辑中调用useState,会混乱顺序。 举一个反例

import { useState } from 'react'
let num = 0
function App() {
  const [count1, setCount1] = useState(0)
  if (num === 0) {
    const [count2, setCount2] = useState(0)
  }
  const [count3, setCount3] = useState(0)
  const handelClick = () => {
    setCount3(count3 + 1)
    setCount2(2)
    num + 1
  }
  return (
    <div onClick={handelClick}>
      hello App {count1}{count3}
    </div>
  )
}
export default App

状态快照

如下例所示

  import { useState } from 'react'
function App() {
  const [count, setCount] = useState(0)
  const handelClick = () => {
    setCount(count + 1)
    setCount(count + 1)
    setCount(count + 1)
    setCount(count + 1)
    console.log(count)
  }

  return <div onClick={handelClick}>hell appccc</div>
}
export default App

setCount只会让下一次函数再执行时用count+1后的值也就是1,而log打印出的也就是当前函数作用域的count值是初始值0.无论调用多少次setCount函数,或是在异步程序,或者return返回值,都只用当前作用域的count值0,所以快照形象的表示像照片一样照下setCount对count改变后的值用于下一次函数执行时。 再举一个原生js中常见的例子

image.png 每次调用延时函数时都会生成一个独立的作用域,并从自身独立作用域中找变量n的值,所以每个作用域不互相影响,也就是闭包的特性。

队列

在全部执行完setCount函数之后在

自动批处理

用途:可捕获更新后的count值 如下所示,在setCount内回调函数中的形参c是从react内部调用实参,每次都从其自己的程序内部作用域找c的值但不会影响回调函数外部count的值,只看定义的作用域,不看调用的作用域,和count快照不同。 其实 setCount(count + 1)也可以理解为回调函数中没有用到c这个形参setCount((c)=>count+1)

import { useState } from 'react'
function App() {
  const [count, setCount] = useState(0)
  const handelClick = () => {
    setCount(count + 1) //0+1
    setCount(count + 1) //0+1
    setCount(count + 1) //0+1
    setCount((c) => c + 1) //0+1
    setCount((c) => c + 1) //1+1
    setCount((c) => c + 1) //2+1
  }
  return <div onClick={handelClick}>hell appccc</div>
}
export default App

状态不可直接修改

举一个反例

import { useState } from 'react'
function App() {
  const [list, setList] = useState([
    { id: 1, name: 'aaa' },
    { id: 2, name: 'bbb' },
    { id: 3, name: 'ccc' },
  ])
  const handelClick = () => {
    list.push({ id: 4, name: ddd })
    setList(list)
  }
  console.log(count)

  return <div onClick={handelClick}>hell appccc</div>
}
export default App

直接对list状态修改再用钩子函数渲染表面看没问题,其实list.push({ id: 4, name: ddd }已经将list修改为末尾加id:4,的对象后当setList(list)会与之前状态比对后发现并无差别,故不会重新渲染。 正确的做法是

  const [list, setList] = useState([
    { id: 1, name: 'aaa' },
    { id: 2, name: 'bbb' },
    { id: 3, name: 'ccc' },
  ])
  const handelClick = () => {
    setList([...list, { id: 4, name: ddd }])
  }

常见的数组和对象的解决方法

image.png

数组
添加元素

setList([...list.slice(0, 1), { id: 4, name: 'ddd' }, ...list.slice(1)])

替换元素

setList( list.map((item) => { if (item.id === 2) return { ...item, name: 'nnnn' } else return { ...item } }) )

排序

结构简单浅拷贝(拷贝不会影响原来的状态,遵守状态不改变,还能保留不需要修改的部分,但是整体拷贝非常耗时,影响性能下面介绍immer会改良)

   const  cloneList=[...list]   //浅拷贝
   cloneList.reverse()
   setList(cloneList)

对象

结构复杂深拷贝 在控制台安装lodash包,调用import { cloneDeep } from 'lodash'

  const cloneInfo=cloneDeep(info)
  cloneInfo.username.first='da'
  setInfo(cloneInfo)

结构简单用扩展运算符

  setInfo({
    ...info,
    username: {
      ...info.username,
      age: 99,
    },
  })

immer简化可变状态(复杂数据再用)

在终端安装immer use-immer两个包,并引入import {useImmer}from'use-immer'

immer是useState的升级版,能够避免useState状态不能直接修改的问题。用法和useState大致相同,区别在useImmer内调用回调函数并传入参数draft 有参数draft生成副本,直接对draft修改即可 数组

    setList((draft)=>
      draft.splice(2,1)
      draft.push{id:4,name:'ddd'}
    )

对象

    setInfo((draft)=>{
      draft.username.first="da"
    })

惰性初始化

为防止 const [count,setCount]=useState(Component(0)) 在调用子组件时多次执行子组件,用回调函数useState(() => Component(0))只执行子组件一次。

function Component(n) {
  console.log(222)
  return (n = n + 1 + 2 + 3 + 4)
}
function App() {
  const [count, setCount] = useState(() => Component(0))
  const handelClick = () => {}
  return <div onClick={handelClick}></div>
}
export default App

状态提升来解决共享问题

多次渲染同一个组件,每个组件都会拥有自己的state。状态是独立的,且不共享的,如下图状态提升来解决共享问题

多次渲染同一个组件,每个组件都会拥有自己的state。状态是独立的,且不共享的

image.png 在Myapp组件中调用MyButton子组件,其中一个子组件count的状态值该改变不会影响到另一个。

当想要在两个子组件中共享状态值时,直接在Myapp父组件中定义变量和事件,在子组件中接收,子组件中的状态state就会同步变化。

状态的重置处理问题

  1. 当组件被销毁时,所对应的状态也会被重置
  2. 当组件位置没有发生改变时,状态是会被保留的。

重置状态:

1.不同的结构体 如当isshow取反后,Button组件结构区别于是否在section标签内 {isshow ? (<Button style={{ border: '1px solid blue' }} />) : ( <section> <Button /></section> )} 那么Button内的state状态就会重置 2.给组件添加key属性 ` {isshow ? ( <Button style={{ border: '1px solid blue' }} key="icon1" /> ) : ( )}

`

计算变量

const [count, setCount] = useState(0) const doubleCount = count * 2

受控组件与非受控组件

通过props控制有父子通信的组件称为受控组件;而通过state控制的自己内部状态控制的组件称为非受控组件 如下的表单和复选框都是受控组件,都需state改变后重新加载

function App() {
  const [value, setValue] = useState('')
  const handelVlivk = (e) => {
    setValue(e.target.value)
  }
  return <input type="text " value={value} onChange={handelVlivk} />
}
function Avvv() {
  const [checked, setCheck] = useState(true)
  const handelccc = (e) => {
    setCheck(e.target.checked)
  }
  return (
    <div>
      <input type="checkbox" checked={checked} onChange={handelccc} />
    </div>
  )
}
export default Avvv