State Hook
大家都知道,在react的类组件中是可以使用状态的——state,那么在函数组件中怎么使用状态呢?在react16.8的之后中提出了hook这个概念。State Hook是一个在函数组件中使用的函数(useState),用于在函数组件中使用状态。并且使用起来会让人感觉神清气爽。
语法
const [a, setA] = useState('参数')
- useState函数有一个参数,这个参数的值表示状态的默认值。用法和vue3的ref('')一样,可以传递任何数据。但是vue3会变成响应式的数据,而react state hook 是对外提供了两个返回值,可以对数据进行操作(get, set)
- 函数的返回值是一个数组,该数组一定包含两项
。第一项:当前状态的值
。第二项:改变状态的函数
注意: 一个函数组件中可以有多个状态,这种做法非常有利于横向切分关注点。
案件
import React, { useState } from 'react' export default function TestStateHook() { // 通过数组解构的方式来进行获取useState的返回值 const [data, setData] = useState(0) return ( <div> <button onClick={() => { setData(data - 1) }}> - 1 </button> <span> {data} </span> <button onClick={() => { setData(data + 1) }}> + 1 </button> </div> ) }
效果
我们在效果中发现,state Hook 中实现了和 class组件一样的效果,并且在调试工具中,也是可以看到hooks中的值的。
原理
在外部使用一个数组来接收所有的state 的状态值。然后最外暴露属性值和一个函数,相当于一个是getter 和 setter
手动实现
let memoizedState = []; // hooks 存放在这个数组 let cursor = 0; // 当前 memoizedState 下标 function useState(initialValue) { memoizedState[cursor] = memoizedState[cursor] || initialValue; const currentCursor = cursor; function setState(newState) { // 如果传入的参数没有变化,不进行更新,不调用render函数 if(newState === memoizedState[currentCursor]) return; memoizedState[currentCursor] = newState; render(); } return [memoizedState[cursor++], setState]; // 返回当前 state,并把 cursor 加 1 }
详情参考
注意的细节
1. useState最好写到函数的起始位置
原因:便于阅读,和我们定义变量一样,hook出现的目的是为了使代码形成代码块,每一段的逻辑分离开来。
案例
import React, { useState } from 'react' // 代码写在function外部,是不行的 const [data, setData] = useState(0) export default function TestStateHook() { return ( <div> <button onClick={() => { setData(data - 1) }}> - 1 </button> <span> {data} </span> <button onClick={() => { setData(data + 1) }}> + 1 </button> </div> ) }
效果
2. useState严禁出现在代码块(判断、循环)中
案列
import React, { useState } from 'react' export default function TestStateHook() { let n = 0; if(n > 0){ // hook 定义在代码判断中 var [data, setData] = useState(n) } return ( <div> <button onClick={() => { setData(data - 1) }}> - 1 </button> <span> {data ? data : 0} </span> <button onClick={() => { setData(data + 1) }}> + 1 </button> </div> ) }
效果
3. useState返回的函数(数组的第二项),引用不变
原因:(节约内存空间)
案例
import React, { useState } from 'react' export default function TestStateHook() { const [data, setData] = useState(0); //定义一个数组来装setData 函数 const arr = [setData] return ( <div> <button onClick={() => { setData(data - 1) arr.push(setData) console.log( arr[0] === arr[1]); }}> - 1 </button> <span> {data} </span> <button onClick={() => { setData(data + 1) arr.push(setData) }}> + 1 </button> </div> ) }
效果
4.使用函数改变数据,若数据和之前的数据完全相等(使用Object.is比较),不会导致重新渲染。
以达到优化效率的目的,自带类组件的纯组件(pureComponent)
案例
import React, { useState } from 'react' export default function TestStateHook() { const [data, setData] = useState(0); // 如果传入的值不同,将会允许改函数 console.log('函数初始化') return ( <div> <button onClick={() => { // 相同的值不会进行更新 setData(data) }}> - 1 </button> <span> {data} </span> <button onClick={() => { setData(data + 1) }}> + 1 </button> </div> ) }
效果
5.使用函数改变数据,传入的值不会和原来的数据进行合并,而是直接替换。
在类组件中,使用 this.setState({a:xxx}) 是会对当前类实例的属性进行混合。但是在state hook 中是不会进行混合的,原因嘛: 官方就是要让你你定义多个 hooks, 将每一个代码块进行分离,从而降低代码的维护成本。
案例
import React, { useState } from 'react' interface obj { a?: number, b?: number } export default function TestStateHook() { const [data, setData] = useState<obj>({ a: 123, b: 321 }); console.log('函数初始化') return ( <div> <p>数字A: {data.a} </p> <p>数字B: {data.b} </p> <button onClick={() => { setData({ b: 42344 }) }}>改变B </button> </div> ) }
效果
上面我们看到了,如果和类组件一样A的值是会存在的,但是意料之外。所以如果某些状态之间没有必然的联系,应该分化为不同的状态,而不要合并成一个对象,对于上面代码如何实现才能实现A不变,修改B的值的时候,把data 展开进行混合。在class组件中,我们使用pureComp 就是每一次都需要传入一个不同引用值的数据,从而来进行数据的更新,否则不会对数据进行更新的。详情查看
6.如果要实现强制刷新组件
1.类组件:使用forceUpdate函数, vue2里面也有一个$foreUpdate来强制刷新组件
2.函数组件:使用一个空对象的useState,如: setData({}), 解释一下为啥传入空对象为啥会强制刷新组件,因为传入的值是一个引用值,我们传入一个空对象的引用地址和原来的对象或者啥值都是不一样的。由于是基于改基础的,所以传入一个空的数组或者函数也是可以做到的。有兴趣的读者可以自行尝试。
7. 和类组件的状态一样,函数组件中改变状态可能是异步的(在DOM事件中)。
多个状态变化会合并以提高效率,此时,不能信任之前的状态,而应该使用回调函数的方式改变状态。如果状态变化要使用到之前的状态,尽量传递函数。
案例
在我门最开始的那个案例, 请关注我们react 中的hooks的值,我们会发现他并不是我们点一次就变化一次的,而是在过一段时间后才进行更新的。
解决办法: 和类组件一样,使用函数来进行更新就可以获取到之前的值。
setData(s => { // 这样就可以获取之前的状态了 console.log(s) return s + 1 })