Fork me on GitHub

react-hooks使用详解

介绍

已经使用 hooks 开发有一段时间了,还是写点东西记一下学到的一些东西吧。

hooks API 个人认为存在的目的就是为了让函数式组件横行天下。

hooks 的基本使用

假设回到以前的class组件的时代,我们需要来做一个定时器的功能(每隔1s+1的那种功能的话),我们可能需要下面这些代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import React from "react";
class B extends React.Component {
state = {
count: 0
};
// 设置一个interval,让它每次把count进行一个+1
componentDidMount() {
this.interval = setInterval(() => {
this.setState({
count: this.state.count + 1
});
}, 1000);
}
// 在组件卸载的时候清除interval属性(不卸载的话会造成内存的泄露)
componentWillUnmount() {
if (this.interval) {
clearInterval(this.interval);
}
}
render() {
return <div>{this.state.count}</div>;
}
}
export default B;

hooks API还没有出来之前我们是无法用function component来完成这些功能的,因为在函数式组件里面是没有this属性的,因此我们也无法使用state去保存函数内部的一些状态。

因此有了react hooks我们可以去使用hooks API去完成这些功能.

我们需要使用useStateuseEffect这些功能来完成这些东西。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React, { useState, useEffect } from "react";
function B() {
// 这里就相当于通过一个es6解构的方式来完成数组的赋值。
const [count, setCount] = useState(0);
// 它会在组件渲染完成之后去执行第一个回调函数
useEffect(() => {
const interval = setInterval(() => {
setCount(c => c + 1);
}, 1000);
// return 的方法会在组件被卸载的时候去执行
return () => clearInterval(interval);
}, []);
return <span>{count}</span>;
}
export default B;

可以看出函数式组件整体上面的代码是要比class组件优化很多的。

useEffect会在组件更新完成之后执行第一个被传入的回调函数,同时也会在组件被卸载的时候去执行return所返回的回调函数。

State hooks API

State hooks API的话一共有两个:

  • useState
  • useReducer

useState

通过上面的Demo我们可以看到一共是接受两个参数:

const [count,setCount] = useState(0);

setCount()的话也是有两个使用形式的:

一种是直接往里面传值(传的值是啥,那么count就为那个值),然后一种就是传一个回调函数,setCount(c=>c+1),回调函数会首先拿到一个值,这个值就是我们使用count所得到最新的值,因此我们可以在这个基础上面再做一些事情。

这两种形式本质上还是有区别的,还是以前面的demo为例子,如果在前面的demo里面我们写的是:

1
2
3
4
5
6
useEffect(() => {
const interval = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(interval);
}, []);

那么你会看到页面会始终停留在 1 这个值那里,因为这个时候整个代码就相当于陷入了一个闭包的陷阱里面,每次使用setInterval去进行count值的获取的时候,它都会拿到每次函数B构建完成之后所瞬间声明的count值(默认为 0),所以这个地方页面所渲染出来的值会永远是1
所以这里我们通过传入回调的方式来规避掉这个 bug。

useReducer

其实useState本质上就是由useReducer演化而来的,useReducer的用法有点类似于redux里面的reducer函数.

我们可以使用useReducer来改写一下上面的demo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import React, { useState, useReducer, useEffect } from "react";
// 它接收两个参数,可以根据action的类型来对state进行更新
function countReducer(state, action) {
switch (action.type) {
case "add":
return state + 1;
case "minus":
return state - 1;
default:
return state;
}
}
function B() {
// ussReducer接收两个参数,一个是一个reducer函数,还有一个是cout的初始值
const [count, dispatchCount] = useReducer(countReducer, 0);
useEffect(() => {
const interval = setInterval(() => {
// 这里通过dispatch这个方法去改变值
dispatchCount({ type: "add" });
}, 1000);
// return 的方法会在组件被卸载的时候去执行
return () => clearInterval(interval);
}, []);
return <span>{count}</span>;
}
export default B;

我们通过dispacth一个action来判断如何去修改一个状态。

useReducer存在的意义就是在使用const [count,setCount] = useState(0)的时候,如果count是一个很复杂的对象,我们使用setCount对其去做出改变是很复杂的,这个时候我们就需要使用useReducer来去修改我们所定义的值的内容。

Effect Hook

先写一个比较简单的案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import React, { useState, useReducer, useEffect } from "react";
// 它接收两个参数,可以根据action的类型来对state进行更新
function countReducer(state, action) {
switch (action.type) {
case "add":
return state + 1;
case "minus":
return state - 1;
default:
return state;
}
}
function B() {
const [count, dispatchCount] = useReducer(countReducer, 0);
const [name, setName] = useState("wd");
useEffect(() => {
console.log("effect invoked");
return () => {
console.log("effect deteched");
};
});
return (
<div>
<input value={name} onChange={e => setName(e.target.value)} />
<button
onClick={() => {
dispatchCount({ type: "add" });
}}
>
{count}
</button>
</div>
);
}
export default B;

我们刚进去页面的时候,会发现控制台输出了effect invoked,然后当我们每次去点击按钮的时候,每点击一次,控制台都会输出:

1
2
effect deteched
effect invoked

但我们去修改input框里面的内容的时候,同样也会输出上面的内容。这说明只要有状态更新这个组件就会重新渲染,每次重新渲染useEffect都要去执行一次。

但是如果我们给useEffect组件加上第二个参数:

1
useEffect(() => {}, []);

这样useEffect就只会在组件渲染完成之后进行一次执行,然后其他时候都不会执行了。

然后只有在组件被注销的时候,会输出effect deteched.

传入的这个数组,是可以在里面传入一些内容的,比如前面有声明的name或者是count,如果在里面放入了 count,那么每次在 count 发生改变的时候这个函数都会去执行一次。同样的传name这个函数也会发生同样的情况。

这里的意思就是这样的,我们传入的这个值,如果在这个周期里面值没有发生改变的话,那么useEffect里面的回调函数就不会执行,同return返回的卸载时执行的函数也不会执行。

如果我们传入空数组,那么useEffect只会在第一次执行。

react官方所给出的意见是,如果我们在useEffect里面使用到了函数式组件内部定义的值,我们都需要把它当成useEffect的第二个数组参数列表里面的一个变量。这个地方我们称之为依赖。

useLayoutEffect

这个hooks apiuseEffect的一个兄弟钩子函数,它的作用和useEffect基本上差不多。

我们直接改写一下demo,在里面加入一个useLayoutEffect函数

1
2
3
4
5
6
7
8
9
10
11
12
13
useEffect(() => {
console.log("effect invoked");
return () => {
console.log("effect deteched");
};
}, [count]);
useLayoutEffect(() => {
console.log("layouteffect invoked");
return () => {
console.log("layouteffect deteched");
};
}, [count]);

当我们每次去点击button按钮的时候,会发现会输出

1
2
3
4
layouteffect deteched
layouteffect invoked
effect deteched
effect invoked

这就可以说明每次useLayoutEffect都在useEffect之前执行。

useEffectuseLayoutEffect本质上的区别就在于,useLayoutEffect会在dom元素执行成为html元素之前执行,而useEffect则会在dom元素执行成html元素之后执行。

一般情况下useLayoutEffect用的比较少(因为如果里面的 js 代码执行时间很长的话),那么那部分dom元素会一直等到useLayoutEffect里面的js代码执行完成之后再去渲染,这样就会很大程度上影响到页面的性能。

Context Hook

Context Hook 应该算的上是 hooks 里面一个比较简单的 API.

其实大致用法就和react-redux里面的那个Provider API 差不多.

Ref Hook

我们可以通过react hooks里面的useRefAPI 来获得某个元素的 dom 节点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import React, { useState, useReducer, useEffect, useRef } from "react";
// 它接收两个参数,可以根据action的类型来对state进行更新
function countReducer(state, action) {
switch (action.type) {
case "add":
return state + 1;
case "minus":
return state - 1;
default:
return state;
}
}
function B() {
const [count, dispatchCount] = useReducer(countReducer, 0);
const [name, setName] = useState("wd");
const inputRef = useRef();
useEffect(() => {
console.log("effect invoked");
// 这个地方输出的就是input这个dom元素
console.log(inputRef.current);
return () => {
console.log("effect deteched");
};
}, [count]);
return (
<div>
<input
ref={inputRef}
value={name}
onChange={e => setName(e.target.value)}
/>
<button
onClick={() => {
dispatchCount({ type: "add" });
}}
>
{count}
</button>
</div>
);
}
export default B;

useImperativeHandle

关于这个API其实也没有过多的介绍,我根据官方写了一个demo来介绍一波怎么使用这个方法来让父组件去拿子组件的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import React , { useImperativeHandle ,useRef, forwardRef, useEffect } from 'react';
function FancyInput(props,ref){
useImperativeHandle(ref,()=>({
test:()=>{
validate()
}
}));
const validate = () => {
console.log('test一波');
}
return(
<div>
<input />
</div>
)
}
FancyInput = forwardRef(FancyInput);
export default () => {
let Fatherref = useRef(null);
useEffect(()=>{
console.log(Fatherref);
Fatherref.current.test();
})
return(
<div>
<FancyInput ref={Fatherref}/>
</div>
)
}

hooks 性能优化

我们先利用前面的一些hooks来写一个demo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import React, { useReducer, useState } from "react";
function countReducer(state, action) {
switch (action.type) {
case "add":
return state + 1;
case "minus":
return state - 1;
default:
return state;
}
}
function MyCountFunc() {
const [count, dispatchCount] = useReducer(countReducer, 0);
const [name, setName] = useState("wd");
const config = {
text: `count is ${count}`,
color: count > 3 ? `red` : `blue`
};
return (
<div>
<input value={name} onChange={e => setName(e.target.value)} />
<Child
config={config}
onButtonClick={() => dispatchCount({ type: "add" })}
/>
</div>
);
}
function Child({ onButtonClick, config }) {
console.log("child render");
return (
<button onClick={onButtonClick} style={{ color: config.color }}>
{config.text}
</button>
);
}
export default MyCountFunc;

通过这个组件我们可以看到每次在父组件里面状态发生更新时,子组件都会跟着同时去进行一个渲染(每次都会输出”child render”)。

而这个时候我们只希望在和子组件相关的props发生改变的时候才去对子组件进行一个重新的渲染,这个时候就需要使用hooks里面的memo了.

它的功能就类似于以前生命周期函数里面的shouldComponentUpdateAPI。

1
2
3
4
5
6
7
8
9
10
import React, { memo } from "react";
const Child = memo(function Child({ onButtonClick, config }) {
console.log("child render");
return (
<button onClick={onButtonClick} style={{ color: config.color }}>
{config.text}
</button>
);
});

这样修改之后我们又会发现,当MyCountFunc方法的输入框里面的值(name)发生改变的时候,子组件又会重新去render一次,这是因为name改变的时候会去调用setName这个方法,然后这个方法就会触发当前方法(MyCountFunc)的重新执行,然后重新执行那么config这个值也会去重新构造一次,这样就会触发Child组件的重新渲染。因为每次方法调用都会形成一个闭包,这样里面的值都会重新去申明一次。

所以这个时候我们需要使用一个新的hooksAPI 去对config这个变量进行一次的包装。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import React, { useReducer, useState, memo, useMemo } from "react";
function countReducer(state, action) {
switch (action.type) {
case "add":
return state + 1;
case "minus":
return state - 1;
default:
return state;
}
}
function MyCountFunc() {
const [count, dispatchCount] = useReducer(countReducer, 0);
const [name, setName] = useState("wd");
const config = useMemo(
() => ({
text: `count is ${count}`,
color: count > 3 ? `red` : `blue`
}),
[count]
);
return (
<div>
<input value={name} onChange={e => setName(e.target.value)} />
<Child
config={config}
onButtonClick={() => dispatchCount({ type: "add" })}
/>
</div>
);
}
const Child = memo(function Child({ onButtonClick, config }) {
console.log("child render");
return (
<button onClick={onButtonClick} style={{ color: config.color }}>
{config.text}
</button>
);
});
export default MyCountFunc;

这个时候你会发现每次改变的时候,子组件仍然会重新去发生渲染,这里造成这个现象的原因其实主要还是MyCountFunc这个组件上面的一个onButtonClick方法导致的。我们也需要对这个方法去进行一个优化

1
2
3
4
5
6
7
8
// 这里也接受第二个参数,也是个空数组,但是这个useCallback方法不接受任何的依赖参数
const handleButtonClick = useCallback(() => dispatchCount({ type: "add" }), []);
return (
<div>
<input value={name} onChange={e => setName(e.target.value)} />
<Child config={config} onButtonClick={handleButtonClick} />
</div>
);

然后到这里之后,我们的优化就完成了。这里就是hooks里面非常重要的一些优化。

其实上面也是可以使用useMemo来取代useCallback的一些功能的。

1
2
3
const handleButtonClick = useMemo(()=>()=>{
dispatch({ type:'add' })
},[])

本质上useCallback实际上就是hooks里面用来优化方法的一种useMemo方法。

-------------本文结束感谢您的阅读-------------