# HOOK 简介
HOOK 是 React16.8.0 之后出现
组件:无状态组件(函数组件)、类组件
类组件中的麻烦:
this 指向问题
繁琐的生命周期
其他问题
HOOK 专门用于增强函数组件的功能(HOOK 在类组件中是不能使用的),使之理论上可以成为类组件的替代品
官方强调:没有必要更改已经完成的类组件,官方目前没有计划取消类组件,只是鼓励使用函数组件
HOOK(钩子)本质上是一个函数(命名上总是以 use 开头),该函数可以挂载任何功能
HOOK 种类:
- useState
- useEffect
- 其他...
# State Hook
State Hook 是一个在函数组件中使用的函数(useState),用于在函数组件中使用状态
useState
- 函数有一个参数,这个参数的值表示状态的默认值
- 函数的返回值是一个数组,该数组一定包含两项
- 第一项:当前状态的值
- 第二项:改变状态的函数
一个函数组件中可以有多个状态,这种做法非常有利于横向切分关注点。
注意的细节
- useState 最好写到函数的起始位置,便于阅读
- useState 严禁出现在代码块(判断、循环)中
- useState 返回的函数(数组的第二项),引用不变(节约内存空间)
- 使用函数改变数据,若数据和之前的数据完全相等(使用 Object.is 比较),不会导致重新渲染,以达到优化效率的目的。
- 使用函数改变数据,传入的值不会和原来的数据进行合并,而是直接替换。
- 如果要实现强制刷新组件
- 类组件:使用 forceUpdate 函数
- 函数组件:使用一个空对象的 useState
- 如果某些状态之间没有必然的联系,应该分化为不同的状态,而不要合并成一个对象
- 和类组件的状态一样,函数组件中改变状态可能是异步的(在 DOM 事件中),多个状态变化会合并以提高效率,此时,不能信任之前的状态,而应该使用回调函数的方式改变状态。如果状态变化要使用到之前的状态,尽量传递函数。
import React, { useState } from "react";
export default function App() {
console.log("App render");
const [n, setN] = useState(0); //使用一个状态,该状态的默认值是0
return (
<div>
<button
onClick={() => {
// setN(n - 1);
// setN(n - 1);
setN((prevN) => prevN - 1); //传入的函数,在事件完成之后统一运行
setN((prevN) => prevN - 1);
}}
>
-
</button>
<span>{n}</span>
<button
onClick={() => {
// setN(n + 1) //不会立即改变,事件运行完成之后一起改变
// setN(n + 1) //此时,n的值仍然是0
setN((prevN) => prevN + 1); //传入的函数,在事件完成之后统一运行
setN((prevN) => prevN + 1);
}}
>
+
</button>
</div>
);
}
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
# Effect Hook
Effect Hook:用于在函数组件中处理副作用
副作用:
- ajax 请求
- 计时器
- 其他异步操作
- 更改真实 DOM 对象
- 本地存储
- 其他会对外部产生影响的操作
函数:useEffect,该函数接收一个函数作为参数,接收的函数就是需要进行副作用操作的函数
细节
- 副作用函数的运行时间点,是在页面完成真实的 UI 渲染之后。因此它的执行是异步的,并且不会阻塞浏览器
- 与类组件中 componentDidMount 和 componentDidUpdate 的区别
- componentDidMount 和 componentDidUpdate,更改了真实 DOM,但是用户还没有看到 UI 更新,同步的。
- useEffect 中的副作用函数,更改了真实 DOM,并且用户已经看到了 UI 更新,异步的。
- 每个函数组件中,可以多次使用 useEffect,但不要放入判断或循环等代码块中。
- useEffect 中的副作用函数,可以有返回值,返回值必须是一个函数,该函数叫做清理函数
- 该函数运行时间点,在每次运行副作用函数之前
- 首次渲染组件不会运行
- 组件被销毁时一定会运行
- useEffect 函数,可以传递第二个参数
- 第二个参数是一个数组
- 数组中记录该副作用的依赖数据
- 当组件重新渲染后,只有依赖数据与上一次不一样的时,才会执行副作用
- 所以,当传递了依赖数据之后,如果数据没有发生变化
- 副作用函数仅在第一次渲染后运行
- 清理函数仅在卸载组件后运行
- 副作用函数中,如果使用了函数上下文中的变量,则由于闭包的影响,会导致副作用函数中变量不会实时变化。
- 副作用函数在每次注册时,会覆盖掉之前的副作用函数,因此,尽量保持副作用函数稳定,否则控制起来会比较复杂。
import React, { useState, useEffect } from "react";
export default function App() {
const [n, setN] = useState(0);
//以下代码属于副作用
// document.title = `计数器:${n}`;
useEffect(() => {
console.log("改变页面标题的副作用操作");
document.title = `计数器:${n}`;
});
useEffect(() => {
console.log("其他的副作用操作");
});
return (
<div>
<span>{n}</span>
<button
onClick={() => {
setN(n + 1);
}}
>
+
</button>
</div>
);
}
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
# 自定义 Hook
State Hook: useState Effect Hook:useEffect
自定义 Hook:将一些常用的、跨越多个组件的 Hook 功能,抽离出去形成一个函数,该函数就是自定义 Hook,自定义 Hook,由于其内部需要使用 Hook 功能,所以它本身也需要按照 Hook 的规则实现:
- 函数名必须以 use 开头
- 调用自定义 Hook 函数时,应该放到顶层
例如:
- 很多组件都需要在第一次加载完成后,获取所有学生数据
- 很多组件都需要在第一次加载完成后,启动一个计时器,然后在组件销毁时卸载
使用 Hook 的时候,如果没有严格按照 Hook 的规则进行,eslint 的一个插件(eslint-plugin-react-hooks)会报出警告
/* eslint "react-hooks/exhaustive-deps": "off" */
import { useEffect } from "react";
/**
* 组件首次渲染后,启动一个Interval计时器
* 组件卸载后,清除该计时器
*/
export default (func, duration) => {
useEffect(() => {
const timer = setInterval(func, duration);
return () => {
clearInterval(timer);
};
}, []);
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Reducer Hook
Flux:Facebook 出品的一个数据流框架
- 规定了数据是单向流动的
- 数据存储在数据仓库中(目前,可以认为 state 就是一个存储数据的仓库)
- action 是改变数据的唯一原因(本质上就是一个对象,action 有两个属性)
- type:字符串,动作的类型
- payload:任意类型,动作发生后的附加信息
- 例如,如果是添加一个学生,action 可以描述为:
{ type:"addStudent", payload: {学生对象的各种信息} }
- 例如,如果要删除一个学生,action 可以描述为:
{ type:"deleteStudent", payload: 学生id }
- 具体改变数据的是一个函数,该函数叫做 reducer
- 该函数接收两个参数
- state:表示当前数据仓库中的数据
- action:描述了如何去改变数据,以及改变数据的一些附加信息
- 该函数必须有一个返回结果,用于表示数据仓库变化之后的数据
- Flux 要求,对象是不可变的,如果返回对象,必须创建新的对象
- reducer 必须是纯函数,不能有任何副作用
- 该函数接收两个参数
- 如果要触发 reducer,不可以直接调用,而是应该调用一个辅助函数 dispatch
- 该函数仅接收一个参数:action
- 该函数会间接去调用 reducer,以达到改变数据的目的
// import React, { useReducer } from 'react'
import React from "react";
import useReducer from "./useReducer";
/**
* 该函数,根据当前的数据,已经action,生成一个新的数据
* @param {*} state
* @param {*} action
*/
function reducer(state, action) {
switch (action.type) {
case "increase":
return state + 1;
case "decrease":
if (state === 0) {
return 0;
}
return state - 1;
default:
return state;
}
}
export default function App() {
const [n, dispatch] = useReducer(reducer, 10, (args) => {
console.log(args);
return 100;
});
return (
<div>
<button
onClick={() => {
dispatch({ type: "decrease" });
}}
>
-
</button>
<span>{n}</span>
<button
onClick={() => {
dispatch({ type: "increase" });
}}
>
+
</button>
</div>
);
}
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
49
import { useState } from "react";
/**
* 通用的useReducer函数
* @param {function} reducer reducer函数,标准格式
* @param {any} initialState 初始状态
* @param {function} initFunc 用于计算初始值的函数
*/
export default function useReducer(reducer, initialState, initFunc) {
const [state, setState] = useState(initFunc ? initFunc(initialState) : initialState);
function dispatch(action) {
const newState = reducer(state, action);
console.log(`日志:n的值 ${state}->${newState}`);
setState(newState);
}
return [state, dispatch];
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# Context Hook
用于获取上下文数据
import React, { useContext } from "react";
const ctx = React.createContext();
// function Test() {
// return <ctx.Consumer>
// {value => <h1>Test,上下文的值:{value}</h1>}
// </ctx.Consumer>
// }
function Test() {
const value = useContext(ctx);
return <h1>Test,上下文的值:{value}</h1>;
}
export default function App() {
return (
<div>
<ctx.Provider value="abc">
<Test />
</ctx.Provider>
</div>
);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# Callback Hook
函数名:useCallback
用于得到一个固定引用值的函数,通常用它进行性能优化
useCallback:
该函数有两个参数:
- 函数,useCallback 会固定该函数的引用,只要依赖项没有发生变化,则始终返回之前函数的地址
- 数组,记录依赖项
该函数返回:引用相对固定的函数地址
import React, { useState, useCallback } from "react";
class Test extends React.PureComponent {
render() {
console.log("Test Render");
return (
<div>
<h1>{this.props.text}</h1>
<button onClick={this.props.onClick}>改变文本</button>
</div>
);
}
}
function Parent() {
console.log("Parent Render");
const [txt, setTxt] = useState(1);
const [n, setN] = useState(0);
const handleClick = useCallback(() => {
setTxt(txt + 1);
}, [txt]);
return (
<div>
{/* 函数的地址每次渲染都发生了变化,导致了子组件跟着重新渲染,若子组件是经过优化的组件,则可能导致优化失效 */}
<Test text={txt} onClick={handleClick} />
<input
type="number"
value={n}
onChange={(e) => {
setN(parseInt(e.target.value));
}}
/>
</div>
);
}
export default function App() {
return (
<div>
<Parent />
</div>
);
}
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
# Memo Hook
用于保持一些比较稳定的数据,通常用于性能优化
如果 React 元素本身的引用没有发生变化,一定不会重新渲染
import React, { useState, useMemo } from "react";
function Item(props) {
// console.log("Item Render " + props.value);
return <li>{props.value}</li>;
}
export default function App() {
const [range] = useState({ min: 1, max: 10000 });
const [n, setN] = useState(0);
const list = useMemo(() => {
const list = [];
for (let i = range.min; i <= range.max; i++) {
list.push(<Item key={i} value={i}></Item>);
}
return list;
}, [range.min, range.max]);
// const list = [];
// for (let i = range.min; i <= range.max; i++) {
// list.push(<Item key={i} value={i}></Item>)
// }
return (
<div>
<ul>{list}</ul>
<input
type="number"
value={n}
onChange={(e) => {
setN(parseInt(e.target.value));
}}
/>
</div>
);
}
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
# Ref Hook
useRef 函数:
- 一个参数:默认值
- 返回一个固定的对象,
{current: 值}
import React, { useState, useRef } from "react";
window.arr = [];
export default function App() {
const inpRef = useRef();
window.arr.push(inpRef);
const [n, setN] = useState(0);
return (
<div>
<input ref={inpRef} type="text" />
<button
onClick={() => {
console.log(inpRef.current.value);
}}
>
得到input的值
</button>
<input
type="number"
value={n}
onChange={(e) => {
setN(e.target.value);
}}
/>
</div>
);
}
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
# ImperativeHandle Hook
函数:useImperativeHandleHook 用于控制子组件向父组件暴露实例方法或属性的 Hook
- 需结合 forwardRef 使用,实现父组件向子组件传递 ref,并在子组件内部通过 useImperativeHandle 定义暴露内容。这种组合解决了函数组件默认无法直接接收 ref 的限制
import React, { useRef, useImperativeHandle } from "react";
function Test(props, ref) {
useImperativeHandle(
ref,
() => {
//如果不给依赖项,则每次运行函数组件都会调用该方法
//如果使用了依赖项,则第一次调用后,会进行缓存,只有依赖项发生变化时才会重新调用函数
//相当于给 ref.current = 1
return {
method() {
console.log("Test Component Called");
},
};
},
[]
);
return <h1>Test Component</h1>;
}
const TestWrapper = React.forwardRef(Test);
// class Test extends React.Component {
// method() {
// console.log("Test method called");
// }
// render() {
// return <h1>Test Component</h1>
// }
// }
export default function App() {
// const [, forceUpdate] = useState({})
const testRef = useRef();
return (
<div>
<TestWrapper ref={testRef} />
<button
onClick={() => {
testRef.current.method();
// console.log(testRef)
// forceUpdate({})
}}
>
点击调用Test组件的method方法
</button>
</div>
);
}
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
49
50
51
# LayoutEffect Hook
useEffect:浏览器渲染完成后,用户看到新的渲染结果之后 useLayoutEffectHook:完成了 DOM 改动,但还没有呈现给用户
应该尽量使用 useEffect,因为它不会导致渲染阻塞,如果出现了问题,再考虑使用 useLayoutEffectHook
import React, { useState, useLayoutEffect, useRef } from "react";
export default function App() {
const [n, setN] = useState(0);
const h1Ref = useRef();
useLayoutEffect(() => {
h1Ref.current.innerText = Math.random().toFixed(2);
});
return (
<div>
<h1 ref={h1Ref}>{n}</h1>
<button
onClick={() => {
setN(n + 1);
}}
>
+
</button>
</div>
);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# DebugValue Hook
useDebugValue:用于将自定义 Hook 的关联数据显示到调试栏
如果创建的自定义 Hook 通用性比较高,可以选择使用 useDebugValue 方便调试
import React, { useState, useEffect, useDebugValue } from "react";
function useTest() {
const [students] = useState([]);
useDebugValue("学生集合");
return students;
}
export default function App() {
useState(0);
useState("abc");
useEffect(() => {
console.log("effect");
}, []);
useTest();
return <div></div>;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17