问题引出:
一个函数组件首次渲染时,肯定会调用函数App(),得到一个虚拟的DIV,进而创建真实的div到页面。
当用户点击button,更新一个数据时,会调用setN(n+1),肯定要重新渲染,所以肯定会再次调用App(),得到新的虚拟DIV。
React对比两个DIV,局部更新。
在这个过程中,每次调用App函数,都会运行useState。那么,App重新执行的时候,执行useState(0)的结果,即n每次的值会不同吗?
function App() {
const [n, setN] = React.useState(0);
console.log(`n:${n}`)
return (
<div className="App">
<p>{n}</p>
<p>
<button onClick={() => setN(n + 1)}>+1</button>
</p>
</div>
);
}
通过log,每次点击按钮,把n+1后,执行的App函数里,每次log出的n的值都是不同的。为什么同样的一句代码,在几次执行的过程中,会有不同的结果呢?

useState()的时候,都会按顺序有自己的index。调用这个函数返回的n就用来读取值。
点击按钮会调用setN(n+1),在这个函数里,把新的值赋给state数组里的对应项,然后再次渲染,只要渲染就肯定会调用App()函数,就又会执行一次useState,再次执行的时候会做一次判断,如果state里已经有值了,就继续使用上次的值,也就是更新之后的值。这样就保证了,在多次渲染时,即使是执行了同一行代码useState(0),也会有最新的结果。
不同的组件都有一份属于自己的state和index,保存在自己的虚拟DOM上。如果点击了按钮,执行setState(),会修改state,并且触发更新,就会再次渲染,再次调用App(),就会再次执行useState,它会读取state[index],而且是最新值,然后生成新的虚拟DOM
setN(n+1)不会改变n
如果点击按钮,修改数据,setN是不会修改原来的n的,而是生成另一个新的n,这两个n是同时存在着的。也就是n是有分身的。
例子:一个n+1按钮,一个log按钮,三秒后打印出n。如果先点+1按钮,再点log,会打印出1,也就是新的n。但是,先点log,再点+1,三秒后打印出的是0,是旧的n。点完+1后,页面上的n马上变成1了。
function App() {
const [n, setN] = useState(0);
return (
<div>
n:{n}
<button onClick={() => {setN(n + 1);}}> n+1 </button>
<button onClick={() => {setTimeout(() => {
console.log(n);
}, 3000);}}> log </button>
</div>
);
}
这就是因为setN并不会改变n。先+1,进行渲染时n就是1,原来的0没用了就会被自动回收。然后再log,打印出的就是1。可是如果先log,三秒后打印n的值。再+1,在第二次渲染App里,n是1,可是第一次渲染的n=0还是存在着的,这个n并没有被改变,所以打印出的还是0
因为只要修改数据,就会触发UI更新,那就一定会再次调用App()。比如第一次调用App,是第一次渲染,n是0.把n+1,就会第二次调用App,是第二次渲染,在这个新的App里,n是1.但是原来的App里的n还是0,并没有被改变。它只是生成了另一份新的数据。
所以就说明,只要我改变数据,渲染一次,就会生成一份n,能不能有一个贯穿始终的状态呢?即,不会生成新的,就在原来的值上修改。有两种方法:
1. useRef
function App() {
const nRef = useRef(0); // {current:0}
return (
<div>
n:{nRef.current} // 读
<button onClick={() => {nRef.current += 1}}> // 写
n+1
</button>
<button onClick={() => {setTimeout(() => {
console.log(nRef.current);
}, 3000);
}}
> log </button>
</div>
);
}
不用useState,因为useState生成的n会执行一次就生成一份。用useRef。数据保存在useRef.current里,这个只有一份。修改也是把useRef.current的值+1,所以这时先log,再+1,打印出的值就是新的n。因为没有生成新的useRef.current,不管更新多少次,都是同一个对象
bug : UI不会自动更新,点+1,页面上的n并不会变化。虽然实际上useRef.current的值已经变了,但是不会更新到UI上。
2. useContext
上下文,它不仅可以贯穿一个组件的始终,也可以贯穿其他组件。它可以让在指定作用域内的所有子孙组件都能拿到最上层组件读写数据接口
总结
每次重新渲染,组件函数就会执行一次。对应所有state都会出现分身。如果不希望有分身,可以使用useRef/useContext。不过useContext一般需要比较重量级的函数,我们一般都是用useRef
useState的注意事项
1. 不能局部更新
function App() {
const [user,setUser] = useState({name:'anqi', age: 18})
const onClick = ()=>{
setUser({
name: 'Jack'
})
}
return (
<div className="App">
<h1>{user.name}</h1>
<h2>{user.age}</h2>
<button onClick={onClick}>Click</button>
</div>
);
}
如果state是一个对象,里边有多个属性。在setUser的时候,如果只修改其中一个属性,会导致另一个变成undefined。因为setState不会自动合并属性。
应该用...语法,把这个对象的所有属性先拷贝过来,再设你要改变的属性。
const onClick = ()=>{
setUser({
...user,
age:20
})
}
2. 不能在原地址上修改
const onClick = ()=>{
user.age=30
setUser(user)
}
比如说,我想修改user.age,先把user.age赋一个新值,在set这个user。会发现,点击按钮,UI并没有更新。
因为React发现user的地址没变,就认为数据没变,就不会去更新UI。
所以要给一个新的地址,给set里传一个对象就可以了,这样就不是同一个地址了。
3. useState也可以接受函数
const [state, setState] = useState(()=>{
return initialState
})
返回一个初始值,效果和直接把初始值传进去一样。接受函数的好处是如果这个初始值的计算比较复杂,函数形式只会执行一次,也就只会在第一次计算。如果是直接传初始值,每次进来都要计算一次。(但是我们一般直接传初始值)
4. setState也可以接受函数
使用场景:我想先把n+1,再把n+2。如果像下边这样写,预期效果是点击按钮后,页面上的n 是3
function App() {
const [n, setN] = useState(0)
const onClick = ()=>{
setN(n+1)
setN(n+2)
}
return (
<div className="App">
<h1>n: {n}</h1>
<button onClick={onClick}>+3</button>
</div>
);
}
可是点击按钮,发现n变成了2。
因为我们之前说过,setN(n+1)不会改变n,它会生成一个新的n。即,执行了第一行setN(n+1),此时n还是0,如果接下来又对这个n操作,setN(n+2),意思是把n加2,就是把0加2,那么结果就是2。不管setN有多少行,都只相当于执行最后一行。
如果想对一个state连续操作,就可以使用函数
const onClick = ()=>{
setN(i=>i+1)
setN(i=>i+2)
}
setN里没有n这个变量。只有一个函数,这个函数代表了一个操作,+1操作,+2操作。并没说把n加1。只是用了占位符i表示了一种操作。
React看到这两行,先把n+1,第二行再把新的值也就是n=1按照+2操作进行。所以页面上n变3
更推荐使用函数写法