什么是组件的状态及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中常见的例子
每次调用延时函数时都会生成一个独立的作用域,并从自身独立作用域中找变量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 }])
}
常见的数组和对象的解决方法
数组
添加元素
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。状态是独立的,且不共享的
在Myapp组件中调用MyButton子组件,其中一个子组件count的状态值该改变不会影响到另一个。
当想要在两个子组件中共享状态值时,直接在Myapp父组件中定义变量和事件,在子组件中接收,子组件中的状态state就会同步变化。
状态的重置处理问题
- 当组件被销毁时,所对应的状态也会被重置
- 当组件位置没有发生改变时,状态是会被保留的。
重置状态:
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