react-hooks2

前一篇讲了useState()和useEffect(),这一篇继续往下

useContext

这个hooks官方文档已经讲的很清楚了,而我自己平时用的也不多(主要还是太菜了,我看antd的源码到处都是useContext),所以也没什么过深的理解,就简单的写下基础用法。

我们直接看官方给的代码

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
const themes = {
light: {
foreground: "#000000",
background: "#eeeeee"
},
dark: {
foreground: "#ffffff",
background: "#222222"
}
};

const ThemeContext = React.createContext(themes.light); // 创建上下文对象,themes.light是默认值

function App() {
return (
<ThemeContext.Provider value={themes.dark}>
<Toolbar />
</ThemeContext.Provider>
);
}

function Toolbar(props) {
return (
<div>
<ThemedButton />
</div>
);
}

function ThemedButton() {
const theme = useContext(ThemeContext); //使用Context对象传递来的值
return (
<button style={{ background: theme.background, color: theme.foreground }}>
I am styled by theme context!
</button>
);
}

然后值得一提的是,官网还提到了一点

1
调用了 useContext 的组件总会在 context 值变化时重新渲染。如果重渲染组件的开销较大,你可以 通过使用 memoization 来优化。

啥意思呢,还是react的渲染机制,在react构建出VDOM树后,如果有某一个组件的state有变化(即调用了setState),就会重新构建VOM树,然后和上一次的VDOM树的快照以diff算法进行比较,然后把比较出来不同的地方再映射到真实DOM上去,所以这里就会有两个性能上的问题。

  1. 只要执行setState(),即使不改变状态数据, 组件也会重新render()
  2. 当前组件重新render, 就会自动重新render子组件 ==> 效率低

这里的context值改变就类似于state改变,会导致子组件中所有调用了useContext的组件重新渲染,类组件中一般有三种解决方式,重写shouldComponentUpdate()方法,用pureComponent,或者React.memo(),那来到函数组件,react自然为我们提供了相关hook——useMemo,这是我们下一个要讲的hook,具体原理等我们讲完useMemo再回头看。

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
import React,{useState,createContext,useContext, memo, useMemo} from 'react';


const ParentContext = createContext();

export default function Parent(){
const [show, setShow] = useState(false)
return (
<React.Fragment>
<ParentContext.Provider value={setShow}>
<Child></Child>
</ParentContext.Provider>
<Other show={show} setShow={setShow}></Other>
</React.Fragment>
)
}

function Child(props){
// O === setShow 方法
const O = useContext(ParentContext)
console.log('Child 组件执行了');
return (
<button onClick={()=>{
O(true)
}}>Child</button>
)
}

function Other(props){
const { show, setShow} = props
return (
show ? <button onClick={()=>{setShow(false)}}>Other</button> : ''
)
}

// 问题是,每次点击button,child组件并没有什么改变但仍然会被重新渲染,怎么优化?

useMemo与useCallback

  1. useMemo

还是先看官网怎么说

1
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

把“创建”函数和依赖项数组作为参数传入 useMemo,它仅会在某个依赖项改变时才重新计算 memoized 值。这种优化有助于避免在每次渲染时都进行高开销的计算。

记住,传入 useMemo 的函数会在渲染期间执行。请不要在这个函数内部执行与渲染无关的操作,诸如副作用这类的操作属于 useEffect 的适用范畴,而不是 useMemo

如果没有提供依赖项数组,useMemo 在每次渲染时都会计算新的值。

想想看,假设我在一个组件里面有一个贼大贼大的计算量,计算机都要算好一会儿的东西需要把他的计算结果渲染在屏幕上,好我初次挂载组件好不容易计算出来了,然后我觉得这个网页的白色太亮眼,刚好他有个夜间模式的选项,我去改一下,得嘞,触发setState(),重新渲染页面,好家伙又要重新算一次,浏览器表示你是不是吃饱没事干折腾我干嘛?

这时候,我们就要用useMemo()来记住我们所计算的值,这样下次渲染的时候,浏览器就会说“哦我记得你个崽子的结果!那我不用算了我直接渲染就行。”

所以,这个memoizedValue可以理解为一个指针,他指向被useMemo()里面的回调函数的返回值,而这个返回值在组件重新渲染后并不会被垃圾回收,会一直存在于内存空间里,直到该组件被销毁,所以我们可以直接拿着memoizedValue去访问这个值,useMemo()第二个参数和useEffect()一样,是一个依赖数组,只有当依赖数组里面的依赖被改变了,useMemo就会再次执行计算并返回一个新的地址。

所以我们再回头看看useMemo是如何优化useContext的,其实上面那串代码写的不太地道,child组件被反复渲染不仅仅是context的问题,还是父组件的state改变的问题,但好在他们的解决方法都是一样的。

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
import React,{useState,createContext,useContext, memo, useMemo} from 'react';


const ParentContext = createContext();

export default function Parent(){
const [show, setShow] = useState(false)
return (
<React.Fragment>
<ParentContext.Provider value={setShow}>
{
useMemo(() => <Child />, [])
}
</ParentContext.Provider>
<Other show={show} setShow={setShow}></Other>
</React.Fragment>
)
}

function Child(props){
// O === setShow 方法
const O = useContext(ParentContext)
console.log('Child 组件执行了');
return (
<button onClick={()=>{
O(true)
}}>Child</button>
)
}

function Other(props){
const { show, setShow} = props
return (
show ? <button onClick={()=>{setShow(false)}}>Other</button> : ''
)
}

哦吼,这样就行了,为啥,一样的道理嘛,我这个Child组件他也是一个值啊,那我父组件每次重新render的时候一看,哎我记得你,不就不会重新render嘛,其实还有一种优化写法,不知道大家能不能想到。和Child这个单词很像的?

放个线分割一下答案


想到没——props.children! 重新渲染无非就是重新render?顶层呢?是react调用了React.createElement()方法重新构建了一个DOM树,而使用children直接渲染子组件可以避免在状态组件中React使用React.createElement(Son) 渲染子组件,所以就不会反复渲染啦。其他关于解决重复渲染的优化可以看这篇文装。 参考链接

接下来就是useCallback()啦

useCallback()

``useCallback(fn, deps)相当于useMemo(() => fn, deps)`

1
2
把内联回调函数及依赖项数组作为参数传入 useCallback,它将返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新。当你把回调函数传递给经过优化的并使用引用相等性去避免非必要渲染(例如 shouldComponentUpdate)的子组件时,它将非常有用。

啥意思呢,就是假设父组件给子组件传递了一个函数嘛,好然后我父组件被一些乱七八糟的东西搞得重新渲染了,得,我子组件又要重新渲染——哪怕用了memo(注意不是useMemo, memo是另一个API,用于比较oldprops和newprops是否相同来觉得是否重新渲染子组件的)也会被重新渲染,为啥? 因为函数在Js里面是一级对象嘛,你父组件每次被重新执行,那我这个函数也会重新生成,那在堆里面的地址就不一样咯,那memo自然不起作用了,这时候用useCallback()优化即可,就不放代码了,因为我觉得直接useMemo()好像更方便一点,然后useCallback还有用作给useEffect()添加函数依赖。

总之这两个API大部分的使用场景还是和性能优化有关系,那我们继续往下看

useReducer

这个hook暂时还没用到过,官网讲的也很明白,以类redux的方式去给函数组件提供state,是useState的替代,但是他配合useContext()有一种牛逼的使用——简化版redux,具体看代码,一步步来,先看看useReducer的具体用法,下面是一个常规的登陆界面的代码

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
function LoginPage() {
const [name, setName] = useState(''); // 用户名
const [pwd, setPwd] = useState(''); // 密码
const [isLoading, setIsLoading] = useState(false); // 是否展示loading,发送请求中
const [error, setError] = useState(''); // 错误信息
const [isLoggedIn, setIsLoggedIn] = useState(false); // 是否登录

const login = (event) => {
event.preventDefault();
setError('');
setIsLoading(true);
login({ name, pwd })
.then(() => {
setIsLoggedIn(true);
setIsLoading(false);
})
.catch((error) => {
// 登录失败: 显示错误信息、清空输入框用户名、密码、清除loading标识
setError(error.message);
setName('');
setPwd('');
setIsLoading(false);
});
}
return (
// 返回页面JSX Element
)
}

怎么样,state太多了,非常难受,那我们直接useReducer —— 集中化状态管理方案

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
49
50
51
52
53
54
const initState = {
name: '',
pwd: '',
isLoading: false,
error: '',
isLoggedIn: false,
}
function loginReducer(state, action) {
switch(action.type) {
case 'login':
return {
...state,
isLoading: true,
error: '',
}
case 'success':
return {
...state,
isLoggedIn: true,
isLoading: false,
}
case 'error':
return {
...state,
error: action.payload.error,
name: '',
pwd: '',
isLoading: false,
}
default:
return state;
}
}
function LoginPage() {
const [state, dispatch] = useReducer(loginReducer, initState);
const { name, pwd, isLoading, error, isLoggedIn } = state;
const login = (event) => {
event.preventDefault();
dispatch({ type: 'login' });
login({ name, pwd })
.then(() => {
dispatch({ type: 'success' });
})
.catch((error) => {
dispatch({
type: 'error'
payload: { error: error.message }
});
});
}
return (
// 返回页面JSX Element
)
}

再之后 加上useContext

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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
// 第一步:创建需要共享的context
const ThemeContext = React.createContext('light');

class App extends React.Component {
render() {
// 第二步:使用 Provider 提供 ThemeContext 的值,Provider所包含的子树都可以直接访问ThemeContext的值
return (
<ThemeContext.Provider value="dark">
<Toolbar />
</ThemeContext.Provider>
);
}
}
// Toolbar 组件并不需要透传 ThemeContext
function Toolbar(props) {
return (
<div>
<ThemedButton />
</div>
);
}

function ThemedButton(props) {
// 第三步:使用共享 Context
const theme = useContext(ThemeContext);
render() {
return <Button theme={theme} />;
}
}

// 定义初始化值
const initState = {
name: '',
pwd: '',
isLoading: false,
error: '',
isLoggedIn: false,
}
// 定义state[业务]处理逻辑 reducer函数
function loginReducer(state, action) {
switch(action.type) {
case 'login':
return {
...state,
isLoading: true,
error: '',
}
case 'success':
return {
...state,
isLoggedIn: true,
isLoading: false,
}
case 'error':
return {
...state,
error: action.payload.error,
name: '',
pwd: '',
isLoading: false,
}
default:
return state;
}
}
// 定义 context函数
const LoginContext = React.createContext();
function LoginPage() {
const [state, dispatch] = useReducer(loginReducer, initState);
const { name, pwd, isLoading, error, isLoggedIn } = state;
const login = (event) => {
event.preventDefault();
dispatch({ type: 'login' });
login({ name, pwd })
.then(() => {
dispatch({ type: 'success' });
})
.catch((error) => {
dispatch({
type: 'error'
payload: { error: error.message }
});
});
}
// 利用 context 共享dispatch
return (
<LoginContext.Provider value={{dispatch}}>
<...>
<LoginButton />
</LoginContext.Provider>
)
}
function LoginButton() {
// 子组件中直接通过context拿到dispatch,出发reducer操作state
const dispatch = useContext(LoginContext);
const click = () => {
if (error) {
// 子组件可以直接 dispatch action
dispatch({
type: 'error'
payload: { error: error.message }
});
}
}
}

哦吼!起飞!还要注意一点就是,useReducer有三个参数,第一个是reducer,第二个是initState,第三个是一个init函数,就是如果我有初始化状态的操作就直接调这个函数就行,具体用法看官方文档,这里不写了。

useRef

就创建一个ref对象,然后可以引用DOM实例,就是可以直接操作真实DOM,所以一般不到万不得已不用ref对象,但是——useRef巧就巧在他有另一个作用,就是如果不在DOM实例里写ref={xxxRef},那这个xxxRef的current属性就是个null,我们可以给它赋一个任意类型的值——就是保存一个在整个组件生命周期都不变的值,之前写useState()的时候简单的提了一下——我们可以拿它获得上一次渲染的值,看一个自定义hook的例子

1
2
3
4
5
6
7
8
9
function usePrevious(value) {
const ref = useRef();

useEffect(() => {
ref.current = value;
});
return ref.current;
}
// 原理?

原理就是ref.current值的改变不会引起组件的重新渲染。顺便注意一下,useRef()每次渲染返回的对象是同一个对象,即内存地址不会重新分配。

其实我愿意把useRef()理解为函数组件的this.,.但不知道这么理解对不对。

useImperativeHandle / useDebugValue

实在没用过

自定义Hook

提公共逻辑嘛,我感觉hooks玩到顶端就是写出很多牛逼的自定义hook,可惜我不会,没啥好介绍的,这是一种思维上的胜出,可惜我还没到这一地步。。。