多组态进行正确记忆
首先看这个代码
import { useState } from 'react'
function App() {
const [count, setCount] = useState(0)
const [count2, setCount2] = useState(0)
const [count3, setCount3] = useState(0)
const handleClick = () => {
setCount(count + 1)
}
return (
<div>
<button onClick={handleClick}>点击</button>
{count}
{count2}
{count3}
</div>
)
}
export default App
这里面使用了多个useState(),在点击事件里只对count做改变,count++,对应的count2和count3不改变,是因为react里有一套状态管理机制,React 内部维护了一个状态池(state pool),用于存储每个组件的状态。当组件重新渲染时,React 会根据组件的顺序查找对应的状态值。
例如在这个代码里,每点击一次,第一个useState内部就会更新保存他的调用顺序,以便在下一次调用的时候直接获取上次执行的结果、
再举一个栗子:
import { useState } from 'react'
let num = 0
function App() {
const [count, setCount] = useState(0)
if (num === 0) {
const [count2, setCount2] = useState(0)
}
const [count3, setCount3] = useState(0)
const handleClick = () => {
setCount(count + 1)
num++
}
return (
<div>
<button onClick={handleClick}>点击</button>
{count}
{count3}
</div>
)
}
export default App
点击以后报错
没点击时顺序为123->点击后num不等于0->顺序改变,2不执行了
因为改变了内部的顺序,搞乱顺序以后就无法记忆了,所以报错
很多类似于useState()的方法都有这个问题,不能改变内部的调用顺序,可以使用eslint来提示
状态的快照
在使用useState对状态变量进行修改时,并没有直接对状态变量修改,而是对你改变的值进行存储,此时打印count还没有改变:
显示的count,也就是setCount改变的count=1,https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=%E4%BD%86%E6%98%AF&pos_id=hDmjFKLM&pos_id=hDmjFKLMuseState是异步的,console.log(count)
会打印当前的 count
值,而不是更新后的值。
如果再次点击,控制台输出的值为1,页面显示的为2
如果把代码改写为:
import { useState } from 'react'
function App() {
const [count, setCount] = useState(0)
const handleClick = () => {
setCount(count + 1)
setCount(count + 1)
setCount(count + 1)
console.log(count)
}
return (
<div>
<button onClick={handleClick}>点击</button>
{count}
</div>
)
}
export default App
第一次点击控制台输出结果是什么?页面是什么?
在handleClick里,这一轮的count无论执行了多少个setCount,值都为初始值,也就是0,那count+1也就等于1了,相当于这样👇:
setCount(1)
setCount(1)
setCount(1)
快照的陷阱
这里其实就是原生js的特性,一些关于闭包和作用域的特性
在点击函数的内部写了一个定时器,2秒后打印count的值,count的值本来应该是0(初始)会不会因为定时器的执行,而变为1
import { useState } from 'react'
function App() {
const [count, setCount] = useState(0)
const handleClick = () => {
setCount(count + 1)
setTimeout(() => {console.log(count)
},2000)
console.log(count)
}
return (
<div>
<button onClick={handleClick}>点击</button>
{count}
</div>
)
}
export default App
可以看出事实并不是,因为有函数自己的作用域内,count的值还没有改变,还是0,所以在延迟两秒后,打印0
JS中的闭包:内层函数使用外层函数的变量,闭包用于实现变量的私有;在handleClick这个内层函数使用了外层函数App的变量count,此时就形成了闭包;进而count这个变量/状态就实现了私有
状态队列与自动批处理
react会等到事件处理函数都运行结束后再处理更新,这样可以提升效率,例如:
在事件处理函数里,调用了三次setState,那么react会不会调用三次渲染函数(App)呢?
import { useState } from 'react'
function App() {
const [count, setCount] = useState(0)
const handleClick = () => {
setCount(count + 1)
setCount(count + 1)
setCount(count + 1)
console.log(count)
}
console.log('如果函数APP渲染了三次,那么我将出现三次');
return (
<div>
<button onClick={handleClick}>点击</button>
{count}
</div>
)
}
export default App
显然是不会的👇
就像点菜是一次全点完再提交菜单
关于useState的使用,其实在里面除了在方法里传入状态变量的新值以外,还可以传入回调函数
例如setCount(count+1)也可以写成setCount((c)=>c+1),效果是一样的
真的一样吗?
import { useState } from 'react'
function App() {
const [count, setCount] = useState(0)
const handleClick = () => {
setCount((c) => c + 1)
setCount((c) => c + 1)
setCount((c) => c + 1)
console.log(count)
}
return (
<div>
<button onClick={handleClick}>点击</button>
{count}
</div>
)
}
export default App
其实这里的setCount里面的回调函数,每次都会在函数内部里返回新值:
setCount((c) => c + 1); // c = 0, 更新为 1
setCount((c) => c + 1); // c = 1, 更新为 2
setCount((c) => c + 1); // c = 2, 更新为 3
https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=%E4%BD%86%E6%98%AF&pos_id=hDmjFKLM&pos_id=hDmjFKLM在事件回调函数这个作用域内,count还打印当前渲染周期的count的值,所以第一次控制台输出的是0,下一轮更新count为3
通过c和count没什么关系这件事可以看出,其实程序setCount(count+1)内部也会被转换为为setCount(c=>count+1),只不过这里c根本没用上,因为count的值依旧是找的快照
严格遵守状态是不可变的
之前我们说state不能直接通过对状态变量的修改做到新渲染,https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=%E4%BD%86%E6%98%AF&pos_id=hDmjFKLM&pos_id=hDmjFKLM在开发时数据较为繁琐,可能一不小心就把数据直接改变了,比如:
import { useState } from 'react'
function App() {
const [list, setList] = useState([
{ id: 1, text: 'aaa' },
{ id: 2, text: 'bbb' },
{ id: 3, text: 'ccc' },
])
const handleClick = () => {
list.push({ id: 4, text: 'ddd' })
setList(list)
}
return (
<div>
<button onClick={handleClick}>点击</button>
<br />
<ul>
{list.map((item) => (
<li key={item.id}>{item.text}</li>
))}
</ul>
</div>
)
}
export default App
你觉得handleClick会成功吗?答案是不会。为什么?继续看!
还有一种情况是当修改的值并没有发生变化时,函数组件并不会重新渲染:
import { useState } from 'react'
function App() {
const [count, setCount] = useState(0)
const handleClick = () => {
setCount(0)
}
console.log('如果函数重新渲染了,那么我将会被打印在控制台两次')
return (
<div>
<button onClick={handleClick}>点击</button>
{count}
</div>
)
}
export default App
当然哈,除了函数默认执行的那次,可以看出函数渲染发现状态变量不变时,不会重新渲染
内部自检就是函数内部会跟踪值的变化,而不是函数多运行了一次,不用管这一次的执行
回到开头的list,因为它对list直接进行修改了,push方法直接对list修改,当你更新一个引用类型的状态时,React 会检查新状态与旧状态是否是同一个引用。如果引用相同,React 会认为状态没有变化,不会触发重新渲染。
重新渲染的条件:为了触发重新渲染,你需要确保新状态是一个新的引用。例如,使用展开运算符(...
)或 Object.assign
创建一个新对象,而不是直接修改原对象。
正确写法只有通过setList并且传递一个新引用才能触发检测机制,发现两次的不同:
import { useState } from 'react'
function App() {
const [list, setList] = useState([
{ id: 1, text: 'aaa' },
{ id: 2, text: 'bbb' },
{ id: 3, text: 'ccc' },
])
const handleClick = () => {
setList([...list,{id:4,text:'ddd'}])
}
return (
<div>
<button onClick={handleClick}>点击</button>
<br />
<ul>
{list.map((item) => (
<li key={item.id}>{item.text}</li>
))}
</ul>
</div>
)
}
export default App
解决方案
所以说像这种引用类型直接修改就会出问题,怎么解决
插入新元素到数组里:
import { useState } from 'react'
function App() {
const [list, setList] = useState([
{ id: 1, text: 'aaa' },
{ id: 2, text: 'bbb' },
{ id: 3, text: 'ccc' },
])
const handleClick = () => {
setList([...list.slice(0,1),{id:4,text:'ddd'},...list.slice(1)])
}
return (
<div>
<button onClick={handleClick}>点击</button>
<br />
<ul>
{list.map((item) => (
<li key={item.id}>{item.text}</li>
))}
</ul>
</div>
)
}
export default App
用map替换
import { useState } from 'react'
function App() {
const [list, setList] = useState([
{ id: 1, text: 'aaa' },
{ id: 2, text: 'bbb' },
{ id: 3, text: 'ccc' },
])
const handleClick = () => {
setList(
list.map((item) => {
if (item.id === 2) {
return { ...item, text: 'ddd' }
} else {
return item
}
})
)
}
return (
<div>
<button onClick={handleClick}>点击</button>
<br />
<ul>
{list.map((item) => (
<li key={item.id}>{item.text}</li>
))}
</ul>
</div>
)
}
export default App
如果想返回一个新的排好顺序的数组,就需要复制一份数据,在复制的数据上操作,不改变原数组
import { useState } from 'react'
function App() {
const [list, setList] = useState([
{ id: 1, text: 'aaa' },
{ id: 2, text: 'bbb' },
{ id: 3, text: 'ccc' },
])
const handleClick = () => {
const clone = [...list]
clone.reverse()
setList(clone)
}
简单的数据可以浅拷贝,复杂的就要深拷贝,深拷贝可以自己写递归,也可以使用lodash插件:
import { useState } from 'react'
import { cloneDeep } from 'lodash'
function App() {
const [list, setList] = useState([
{ id: 1, text: 'aaa' },
{ id: 2, text: 'bbb' },
{ id: 3, text: 'ccc' },
])
const handleClick = () => {
const clone = cloneDeep(list)
clone.reverse()
setList(clone)
}
return (
<div>
<button onClick={handleClick}>点击</button>
<br />
<ul>
{list.map((item) => (
<li key={item.id}>{item.text}</li>
))}
</ul>
</div>
)
}
export default App
如果是对象的话:
import { useState } from 'react'
import { cloneDeep } from 'lodash'
function App() {
const [info, setInfo] = useState({
username: 'xiaoming',
age: 20,
})
const handleClick = () => {
setInfo({ ...info, username: 'xiaohong' })
}
return (
<div>
<button onClick={handleClick}>点击</button>
<div>{JSON.stringify(info)}</div>
</div>
)
}
export default App
数据更新成功
多层的就要使用嵌套展开运算符
import { useState } from 'react'
import { cloneDeep } from 'lodash'
function App() {
const [info, setInfo] = useState({
username: {
first: 'xiao',
last:'ming'
},
age: 20,
})
const handleClick = () => {
setInfo({ ...info, username: {...info.username,first:'da',last:'shabi'} })
}
return (
<div>
<button onClick={handleClick}>点击</button>
<div>{JSON.stringify(info)}</div>
</div>
)
}
export default App
// export default App
也可以使用深拷贝
import { useState } from 'react'
import { cloneDeep } from 'lodash'
function App() {
const [info, setInfo] = useState({
username: {
first: 'xiao',
last: 'ming',
},
age: 20,
})
const handleClick = () => {
const clone = cloneDeep(info)
clone.username.first = 'da'
clone.username.last = 'shabi'
setInfo(clone)
}
return (
<div>
<button onClick={handleClick}>点击</button>
<div>{JSON.stringify(info)}</div>
</div>
)
}
export default App
https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=%E4%BD%86%E6%98%AF&pos_id=hDmjFKLM&pos_id=hDmjFKLM深拷贝性能不好
解决性能问题
把useState改为useImmer,里面的参数draft起到一个整合作用
当你尝试修改对象时,对对象做一个底层拦截,把对象里面修改的数据和没修改的原数据做整合
对象也一样
import { useImmer } from 'use-immer'
function App() {
const [info, setInfo] = useImmer({
username: {
first: 'xiao',
last: 'ming',
},
age: 20,
})
const handleClick = () => {
setInfo((draft) => {
draft.username.first = 'da'
draft.username.last = 'shabi'
})
}
return (
<div>
<button onClick={handleClick}>点击</button>
<div>{JSON.stringify(info)}</div>
</div>
)
}
export default App
惰性初始化状态的值
一种简单的性能提升方案
当useState的初始值是一个计算的函数的返回值时,如果直接引入,会发现之后每次修改状态变量,都会触发计算函数,很消耗资源
解决这个问题的方法就是不直接调用,写成回调函数的形式,实现惰性初始化·:
import { func } from 'prop-types'
import { useState } from 'react'
function computed(n) {
console.log('我每出现一次,就代表useState的初始值多计算一次')
return n + 1 + 2 + 3
}
function App() {
const [count, setCount] = useState(()=>computed(0))
const handleClick = () => {
setCount(count+1)
}
return (
<div>
<button onClick={handleClick}>点击</button>
{count}
</div>
)
}
export default App
状态提升来解决共享问题
组件本身是独立的,函数1里的状态变量的状态和函数2里的状态变量的状态不同步,怎么让他同步?
使用状态提升,通过父子通信的形式共享状态
import { useState } from 'react'
function Button({ count, onClick }) {
const handleClick = () => {
setCount(count + 1)
}
return (
<div>
<button onClick={onClick}>点我</button>
{count}
</div>
)
}
function App() {
const [count, setCount] = useState(0)
const handleClick = () => {
setCount(count + 1)
}
return (
<div>
<Button count={count} onClick={handleClick} />
<Button count={count} onClick={handleClick} />
</div>
)
}
export default App
状态的重置处理问题
组件被销毁时,状态会重置
例如下面的代码里,显示切换隐藏后,count在渲染的时候是从头开始渲染的,也就是重置
import { useState } from 'react'
function Counter() {
const [count, setCount] = useState(0)
const handleClick = () => {
setCount(count + 1)
}
return (
<div>
<button onClick={handleClick}>计数</button>
{count}
</div>
)
}
function App() {
const [show, setShow] = useState(true)
const handleClick = () => {
setShow(!show)
}
return <div>
<button onClick={handleClick}>显示切换隐藏</button>
{show && <Counter />}</div>
}
export default App
当组件位置没有发生改变时,状态保留
import { useState } from 'react'
function Counter({style}) {
const [count, setCount] = useState(0)
const handleClick = () => {
setCount(count + 1)
}
return (
<div>
<button onClick={handleClick} style={style}>计数</button>
{count}
</div>
)
}
function App() {
const [style, setStyle] = useState(false)
const handleClick = () => {
setStyle(!style)
}
return (
<div>
<button onClick={handleClick}>显示切换样式</button>
{style ? <Counter style={{ border: '1px red solid' }} /> : <Counter />}
</div>
)
}
export default App
这时候发现计数5并没有被销毁,为什么?
因为这两个组件处在同一个结构里,相当于【位置没有发生改变】
如果这么写:
import { useState } from 'react'
function Counter({style}) {
const [count, setCount] = useState(0)
const handleClick = () => {
setCount(count + 1)
}
return (
<div>
<button onClick={handleClick} style={style}>计数</button>
{count}
</div>
)
}
function App() {
const [style, setStyle] = useState(false)
const handleClick = () => {
setStyle(!style)
}
return (
<div>
<button onClick={handleClick}>显示切换样式</button>
{style ? <Counter style={{ border: '1px red solid' }} /> :<div> <Counter /></div>}
</div>
)
}
export default App
这样就不可以了,因为结构不同,react会认为是两个独立的组件,相当于把前面的结构完全销毁,然后赋予新的结构,所以会重置
如果给这两个组件都包上div,那么就属于同一结构,状态保留
import { useState } from 'react'
function Counter({style}) {
const [count, setCount] = useState(0)
const handleClick = () => {
setCount(count + 1)
}
return (
<div>
<button onClick={handleClick} style={style}>计数</button>
{count}
</div>
)
}
function App() {
const [style, setStyle] = useState(false)
const handleClick = () => {
setStyle(!style)
}
return (
<div>
<button onClick={handleClick}>显示切换样式</button>
{style ? (
<div>
<Counter style={{ border: '1px red solid' }} />
</div>
) : (
<div>
<Counter />
</div>
)}
</div>
)
}
export default App
标签必须一模一样,例如把div改成section,虽然是一类标签https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=%E4%BD%86%E6%98%AF&pos_id=hDmjFKLM&pos_id=hDmjFKLM名字不同,也会重置
<section>
<Counter style={{ border: '1px red solid' }} />
</section>
也就是说只要文档结构树不一样,就会重置
还有一种情况,如果位置不变,你想让他重置,可以添加key属性
import { useState } from 'react'
function Counter({ style }) {
const [count, setCount] = useState(0)
const handleClick = () => {
setCount(count + 1)
}
return (
<div>
<button onClick={handleClick} style={style}>
计数
</button>
{count}
</div>
)
}
function App() {
const [style, setStyle] = useState(false)
const handleClick = () => {
setStyle(!style)
}
return (
<div>
<button onClick={handleClick}>显示切换样式</button>
{style ? (
<section>
<Counter style={{ border: '1px red solid' }} key='con1'/>/*{添加key属性}*/
</section>
) : (
<div>
<Counter key='con2' />
</div>
)}
</div>
)
}
export default App
利用状态产生计算变量
import { useState } from 'react'
function App() {
const [count, setCount] = useState(0)
const [doubleCount, setDoubleCount] = useState(count * 2)
const handleClick = () => {
setCount(count + 1)
}
return (
<div>
<button onClick={handleClick}>计数</button>
{count}
</div>
)
}
export default App
doubleCount设置的初始值是count*2,也就是0,https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=%E4%BD%86%E6%98%AF&pos_id=hDmjFKLM&pos_id=hDmjFKLM在之后的count改变的时候,doubleCount不包含跟着改变,因为他们两者并没有建立依赖关系
又有人问了(实际上没有)我这么写不就对了吗,让count每次改变的时候,doubleCount也跟着改变就好了:
import { useState } from 'react'
function App() {
const [count, setCount] = useState(0)
const [doubleCount, setDoubleCount] = useState(count * 2)
const handleClick = () => {
setCount(count + 1)
setDoubleCount((count+1)*2)
}
return (
<div>
<button onClick={handleClick}>计数</button>
{count}{doubleCount}
</div>
)
}
export default App
实际上确实可以
但是不建议这么写,显得代码很冗余
如果是这样的话,其实doubleCount就可以不用useState了
这种b依赖a的改变而改变的变量,其实可以不设置为状态变量的捏
import { useState } from 'react'
function App() {
const [count, setCount] = useState(0)
const doubleCount=count * 2
const handleClick = () => {
setCount(count + 1)
}
return (
<div>
<button onClick={handleClick}>计数</button>
{count},{doubleCount}
</div>
)
}
export default App
效果和上面的代码一样
受控组件和非受控组件
受控组件:通过props控制组件的变化,例如这就是个受控组件👇
import { useState } from 'react'
function App() {
const [value, setValue] = useState('')
const handleChange = () => {
}
return (
<div>
<input type="text" value={value} onChange={handleChange} />
{value}
</div>
)
}
export default App
此时你的input是个受控的组件,你的value被控制了,你不能通过输入框决定输入内容了
得通过组件内部修改:
import { useState } from 'react'
function App() {
const [value, setValue] = useState('')
const handleChange = (e) => {
setValue(e.target.value)
}
return (
<div>
<input type="text" value={value} onChange={handleChange} />
{value}
</div>
)
}
export default App
同理,表单选择也可以用组件控制:
import { useState } from 'react'
function App() {
const [checked,setChecked] = useState(false)
const handleChange = (e) => {
setChecked(e.target.checked)
}
return (
<div>
<input type="checkbox" checked={checked} onChange={handleChange} />
</div>
)
}
export default App