React Hook
新出现背景
类组件的问题
- 复用组件状态难,高阶组件+渲染属性
providers customers
,等一堆工具都是为了解决这个问题,但是造成了很严重的理解成本和组件嵌套地狱 - 生命周期带来的负面影响,逻辑拆分严重
- This 的指向问题
函数组件的局限
- 之前函数组件没有
state
和 生命周期,导致使用场景有限
React Hook
Hooks
是 React 16.8
新增的特性,它可以让你在不编写 class
的情况下使用 state
以及其他的 React
特性,无需转化成类组件
Hook
的使用和实践
useState
和 Hook
的闭包机制
// hook 组件
function Counter() {
const [count, setCount] = useState(0);
const log = () => {
setCount(count + 1);
setTimeout(() => {
console.log(count);
}, 3000);
};
return (
<div>
<p>You clicked {
count} times</p>
<button onClick={
log}>Click me</button> </div>
);
}
// 等效的类组件
class Counter extends Component {
state = {
count: 0 };
log = () => {
this.setState({
count: this.state.count + 1,
});
setTimeout(() => {
console.log(this.state.count);
}, 3000);
};
render() {
return (
<div>
<p>You clicked {
this.state.count} times</p>
<button onClick={
this.log}>Click me</button> </div>
);
}
}
快速点击下的情况下,想想 Hook
组件和函数式组件控制台打印出来的是什么?
- 类组件打印出来的是
3 3 3
Class
组件的state
是不可变的,通过setState
返回一个新的引用,this.state
指向一个新的引用
setTimeout
执行的时候,通过this
获取最新的state
引用,所以这个输出都是3
- 函数组件打印的结果是
0 1 2
函数组件闭包机制,函数组件每一次渲染都有独立的props
和state
每一次渲染都有独立的事件处理函数
每一次渲染的状态不会受到后面事件处理的影响
函数组件渲染拆解
既然每次渲染都是一个独立的闭包,可以尝试代码拆解函数式组件的渲染过程
参考React实战视频讲解:进入学习
// 第一次点击
function Counter() {
const [0, setCount] = useState(0);
const log = () => {
setCount(0 + 1);
// 只能获取这次点击按钮的 state
setTimeout(() => {
console.log(0);
}, 3000);
};
}
// 第二次点击
function Counter() {
const [1, setCount] = useState(0);
const log = () => {
setCount(1 + 1);
setTimeout(() => {
console.log(1);
}, 3000);
};
}
// 第三次点击
function Counter() {
const [2, setCount] = useState(0);
const log = () => {
setCount(2 + 1);
setTimeout(() => {
console.log(2);
}, 3000);
};
}
-
三次点击,共
4
次渲染,count
从0
变为3
-
页面第一次渲染,页面看到的
count = 0
-
第一次点击,事件处理器获取的
count = 0
,count
变成1
, 第二次渲染,渲染后页面看到count = 1
,对应上述代码第一次点击 -
第二次点击,事件处理器获取的
count = 1
,count
变成2
, 第三次渲染,渲染后页面看到count = 2
,对应上述代码第二次点击 -
第三次点击,事件处理器获取的
count = 2
,count
变成3
, 第四次渲染,渲染后页面看到count = 3
,对应上述代码第三次点击
让函数式组件也可以输出 3 3 3
有种比较简单并且能解决问题的方案,借用 useRef
useRef
返回一个可变的ref
对象,其current
属性被初始化为传入的参数(initialValue)
useRef
返回的ref
对象在组件的整个生命周期内保持不变,也就是说每次重新渲染函数组件时,返回的ref
对象都是同一个useRef
可以类比成类组件实例化后的this
,在组件没有销毁的返回的引用都是同一个
function Counter() {
const count = useRef(0);
const log = () => {
count.current++;
setTimeout(() => {
console.log(count.current);
}, 3000);
};
return (
<div>
<p>You clicked {
count.current} times</p>
<button onClick={
log}>Click me</button> </div>
);
}
- 这样修改一下,控制台输出的确实是
3 3 3
- ? 既然
Ref
对象整个生命周期都不变,修改current
属性也只是修改属性,那除了打印,这里的You clicked 0 times
,点击三次,会变成3
么? - 显然不能,这个组件没有任何的属性和状态改变,会重新渲染才怪,所以这里虽然点击了
3
次,但是不会像useState
一样,渲染4
次,这里只会渲染1
次,然后看到的都是You clicked 0 times
- 修复一个问题把另外一个更大的问题引进来,这很程序员。。。
useEffect
通过 useRef
虽然能解决打印的问题,但是页面渲染是不对的,这里还是使用 useState
的方案,配合 useEffect
可以实现我们想要的效果
function useEffect(effect: EffectCallback, deps?: DependencyList): void;
- 看下
useEffect
的签名,effect
是函数类型,并且必填, 还有第二个可选参数,类型是只读数组 useEffect
是处理副作用的,其执行时机在 每次Render
渲染完毕后,换句话说就是每次渲染都会执行,在真实DOM
操作完毕后。
配合这个 hook
, 如果每次 state
改变后渲染完之后,把 ref
里面的值更新,然后控制台打印 ref
的值,
function Counter() {
const [count, setCount] = useState(0);
const currentCount = useRef(count);
useEffect(() => {
currentCount.current = count;
});
const log = () => {
setCount(count + 1);
setTimeout(() => {
console.log(currentCount.current);
}, 3000);
};
return (
<div>
<p>You clicked {
count} times