React Hooks 是 React 16.8 引入的新特性,允许我们在不使用 Class 的前提下使用 state 和其他特性。React Hooks 要解决的问题是状态共享,是继 render-props 和 higher-order components 之后的第三种状态逻辑复用方案,不会产生 JSX 嵌套地狱问题。
为什么会有Hooks?
介绍Hooks之前,首先要给大家说一下React的组件创建方式,一种是类组件,一种是纯函数组件,并且React团队希望,组件不要变成复杂的容器,最好只是数据流的管道。开发者根据需要,组合管道即可。也就是说组件的最佳写法应该是函数,而不是类。。
函数组件比类组件更加方便实现业务逻辑代码的分离和组件的复用,函数组件也比类组件轻量,没有react hooks之前,函数组件是无法实现LocalState的,这导致有localstate状态的组件无法用函数组件来书写,这限制了函数组件的应用范围,而react hooks扩展了函数组件的能力。可是在使用的过程中,也要注意下面这些问题,否则就会掉进坑里,造成性能损失。按照下面的方法做,,才能避开这些陷阱。
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
27
28
29
30
31
32
33
34import React, {useState,useCallback} from "react"; // 测试每次状态改变时,方法是不是重新分配内存 let testFooMemoAlloc = new Set(); const Page = (props:any) => { console.log('每次状态改变,函数组件从头开始执行') const [count, setCount] = useState(0); const calc = () => { setCount(count + 1); } const bar = { a:1, b:2, c: '与状态无关的变量定义' } const doFoo = () => { console.log('与状态无关的方法'); } testFooMemoAlloc.add(doFoo) return ( <> <button onClick={calc}>加1</button> <p>count:{count}</p> <p>testFooMemoAlloc.size增加的话,说明每次都重新分配了内存:{testFooMemoAlloc.size}</p> </> ) } export default Page;
与改变状态相关的变量和方法,必须放在hooks组件内,而无状态无关的变量和方法,可以提取到函数组件外,避免每次状态更新,都重新分配内存。也可以分别使用useMemo和useCallback包裹变量与函数,也能达到同样的效果,后面会讲。
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
35import React, {useState,useCallback} from "react"; // 测试每次状态改变时,方法是不是重新分配内存 let testFooMemoAlloc = new Set(); const bar = { a:1, b:2, c: '与状态无关的变量定义' } const doFoo = () => { console.log('与状态无关的方法'); } const Page = (props:any) => { console.log('每次状态改变,函数组件从头开始执行') const [count, setCount] = useState(0); const calc = () => { setCount(count + 1); } testFooMemoAlloc.add(doFoo) return ( <> <button onClick={calc}>加1</button> <p>count:{count}</p> <p>testFooMemoAlloc.size增加的话,说明每次都重新分配了内存:{testFooMemoAlloc.size}</p> </> ) } export default Page;
2. 用memo对子组件进行包装
父组件引入子组件,会造成一些不必要的重复渲染,每次父组件更新count,子组件都会更新。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19import React,{useState} from "react"; const Child = (props:any) => { console.log('子组件?') return( <div>我是一个子组件</div> ); } const Page = (props:any) => { const [count, setCount] = useState(0); return ( <> <button onClick={(e) => { setCount(count+1) }}>加1</button> <p>count:{count}</p> <Child /> </> ) } export default Page;
使用memo,count变化子组件没有更新
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19import React,{useState,memo} from "react"; const Child = memo((props:any) => { console.log('子组件?') return( <div>我是一个子组件</div> ); }) const Page = (props:any) => { const [count, setCount] = useState(0); return ( <> <button onClick={(e) => { setCount(count+1) }}>加1</button> <p>count:{count}</p> <Child /> </> ) } export default Page;
给memo传入第二个参数,开启对象深度比较。当子组件传递的属性值未发生改变时,子组件不会做无意义的render。
memo不仅适用于函数组件,也适用于class组件,是一个高阶组件,默认情况下只会对复杂对象做浅层比较,如果想做深度比较,可以传入第二个参数。与shouldComponentUpdate
不同的是,deepCompare返回true
时,不会触发 render,如果返回false
,则会。而shouldComponentUpdate
刚好与其相反。
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
36import React, {useState, memo } from "react"; import deepCompare from "./deepCompare"; const Child = memo((props:any) => { console.log('子组件') return ( <> <div>我是一个子组件</div> <div>{ props.fooObj.a}</div> </> ); }, deepCompare) const Page = (props:any) => { const [count, setCount] = useState(0); const [fooObj, setFooObj] = useState({ a: 1, b: { c: 2 } }) console.log('页面开始渲染') const calc = () => { setCount(count + 1); if (count === 3) { setFooObj({ b: { c: 2 }, a: count }) } } const doBar = () => { console.log('给子组件传递方法,测试一下是否会引起不必须的渲染') } return ( <> <button onClick={calc}>加1</button> <p>count:{count}</p> <Child fooObj={fooObj} doBar={doBar} /> </> ) } export default Page;
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
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131// 深度比较两个对象是否相等 export default function deepCompare(prevProps: any, nextProps: any) { const len: number = arguments.length; let leftChain: any[] = []; let rightChain: any = []; // // console.log({ arguments }); // if (len < 2) { // console.log('需要传入2个对象,才能进行两个对象的属性对比'); return true; } // for (let i = 1; i < len; i++) { // leftChain = []; // rightChain = []; console.log({ prevProps, nextProps }); if (!compare2Objects(prevProps, nextProps, leftChain, rightChain)) { // console.log('两个对象不相等'); return false; } // } // console.log('两个对象相等'); return true; } function compare2Objects(prevProps: any, nextProps: any, leftChain: any, rightChain: any) { var p; // 两个值都为为NaN时,在js中是不相等的, 而在这里认为相等才是合理的 if (isNaN(prevProps) && isNaN(nextProps) && typeof prevProps === 'number' && typeof nextProps === 'number') { return true; } // 原始值比较 if (prevProps === nextProps) { console.log('原始值', prevProps, nextProps); return true; } // 构造类型比较 if ( (typeof prevProps === 'function' && typeof nextProps === 'function') || (prevProps instanceof Date && nextProps instanceof Date) || (prevProps instanceof RegExp && nextProps instanceof RegExp) || (prevProps instanceof String && nextProps instanceof String) || (prevProps instanceof Number && nextProps instanceof Number) ) { console.log('function', prevProps.toString() === nextProps.toString()); return prevProps.toString() === nextProps.toString(); } // 两个比较变量的值如果是null和undefined,在这里会退出 if (!(prevProps instanceof Object && nextProps instanceof Object)) { console.log(prevProps, nextProps, 'prevProps instanceof Object && nextProps instanceof Object'); return false; } if (prevProps.isPrototypeOf(nextProps) || nextProps.isPrototypeOf(prevProps)) { console.log('prevProps.isPrototypeOf(nextProps) || nextProps.isPrototypeOf(prevProps)'); return false; } // 构造器不相等则两个对象不相等 if (prevProps.constructor !== nextProps.constructor) { console.log('prevProps.constructor !== nextProps.constructor'); return false; } // 原型不相等则两个对象不相等 if (prevProps.prototype !== nextProps.prototype) { console.log('prevProps.prototype !== nextProps.prototype'); return false; } if (leftChain.indexOf(prevProps) > -1 || rightChain.indexOf(nextProps) > -1) { console.log('leftChain.indexOf(prevProps) > -1 || rightChain.indexOf(nextProps) > -1'); return false; } // 遍历下次的属性对象,优先比较不相等的情形 for (p in nextProps) { if (nextProps.hasOwnProperty(p) !== prevProps.hasOwnProperty(p)) { console.log('nextProps.hasOwnProperty(p) !== prevProps.hasOwnProperty(p)'); return false; } else if (typeof nextProps[p] !== typeof prevProps[p]) { console.log('typeof nextProps[p] !== typeof prevProps[p]'); return false; } } // console.log('p in prevProps'); // 遍历上次的属性对象,优先比较不相等的情形 for (p in prevProps) { // 是否都存在某个属性值 if (nextProps.hasOwnProperty(p) !== prevProps.hasOwnProperty(p)) { console.log('nextProps.hasOwnProperty(p) !== prevProps.hasOwnProperty(p)'); return false; } // 属性值的类型是否相等 else if (typeof nextProps[p] !== typeof prevProps[p]) { console.log('typeof nextProps[p] !== typeof prevProps[p]'); return false; } console.log('typeof prevProps[p]', typeof prevProps[p]); switch (typeof prevProps[p]) { // 对象类型和函数类型的处理 case 'object': case 'function': leftChain.push(prevProps); rightChain.push(nextProps); if (!compare2Objects(prevProps[p], nextProps[p], leftChain, rightChain)) { console.log('!compare2Objects(prevProps[p], nextProps[p], leftChain, rightChain)'); return false; } leftChain.pop(); rightChain.pop(); break; default: // 基础类型的处理 if (prevProps[p] !== nextProps[p]) { return false; } break; } } return true; }
3.用useCallback对组件方法进行包装
当父组件传递方法给子组件的时候,memo好像没什么效果,无论是用const定义的方法,还在用箭头函数或者bind定义的方法,子组件还是执行了
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
58import React, { useState,memo } from 'react'; //子组件会有不必要渲染的例子 interface ChildProps { changeName: ()=>void; } const FunChild = ({ changeName}: ChildProps): JSX.Element => { console.log('普通函数子组件') return( <> <div>我是普通函数子组件</div> <button onClick={changeName}>普通函数子组件按钮</button> </> ); } const FunMemo = memo(FunChild); const ArrowChild = ({ changeName}: ChildProps): JSX.Element => { console.log('箭头函数子组件') return( <> <div>我是箭头函数子组件</div> <button onClick={changeName.bind(null,'test')}>箭头函数子组件按钮</button> </> ); } const ArrowMemo = memo(ArrowChild); const BindChild = ({ changeName}: ChildProps): JSX.Element => { console.log('Bind函数子组件') return( <> <div>我是Bind函数子组件</div> <button onClick={changeName}>Bind函数子组件按钮</button> </> ); } const BindMemo = memo(BindChild); const Page = (props:any) => { const [count, setCount] = useState(0); const name = "test"; const changeName = function() { console.log('测试给子组件传递方法,使用useCallback后,子组件是否还会进行无效渲染'); } return ( <> <button onClick={(e) => { setCount(count+1) }}>加1</button> <p>count:{count}</p> <ArrowMemo changeName={()=>changeName()}/> <BindMemo changeName={changeName.bind(null)}/> <FunMemo changeName={changeName} /> </> ) } export default Page;
使用useCallback,参数为[],页面初始渲染后,改变count的值,传递普通函数的子组件不再渲染, 传递箭头函数和bind方式书写的方法的子组件还是会渲染
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
58import React, { useState,memo ,useCallback} from 'react'; //子组件会有不必要渲染的例子 interface ChildProps { changeName: ()=>void; } const FunChild = ({ changeName}: ChildProps): JSX.Element => { console.log('普通函数子组件') return( <> <div>我是普通函数子组件</div> <button onClick={changeName}>普通函数子组件按钮</button> </> ); } const FunMemo = memo(FunChild); const ArrowChild = ({ changeName}: ChildProps): JSX.Element => { console.log('箭头函数子组件') return( <> <div>我是箭头函数子组件</div> <button onClick={changeName.bind(null,'test')}>箭头函数子组件按钮</button> </> ); } const ArrowMemo = memo(ArrowChild); const BindChild = ({ changeName}: ChildProps): JSX.Element => { console.log('Bind函数子组件') return( <> <div>我是Bind函数子组件</div> <button onClick={changeName}>Bind函数子组件按钮</button> </> ); } const BindMemo = memo(BindChild); const Page = (props:any) => { const [count, setCount] = useState(0); const name = "test"; const changeName = useCallback(() => { console.log('测试给子组件传递方法,使用useCallback后,子组件是否还会进行无效渲染'); },[]) return ( <> <button onClick={(e) => { setCount(count+1) }}>加1</button> <p>count:{count}</p> <ArrowMemo changeName={()=>changeName()}/> <BindMemo changeName={changeName.bind(null)}/> <FunMemo changeName={changeName} /> </> ) } export default Page;
4.用useMemo对组件中的对象变量进行包装
在子组件使用了memo,useCallback的情况下,给子组件传递一个对象属性,对象值和方法都未发生改变的情况下,父组件无关状态变更,子组件也会重新渲染。
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
35import React, { useState,memo ,useCallback} from 'react'; //子组件会有不必要渲染的例子-使用了memo,useCallback的情况下,给子组件传递一个对象属性值 interface ChildProps { childStyle: { color: string; fontSize: string;}; changeName: ()=>void; } const FunChild = ({ childStyle,changeName}: ChildProps): JSX.Element => { console.log('普通函数子组件') return( <> <div style={childStyle}>我是普通函数子组件</div> <button onClick={changeName}>普通函数子组件按钮</button> </> ); } const FunMemo = memo(FunChild); const Page = (props:any) => { const [count, setCount] = useState(0); const childStyle = {color:'green',fontSize:'16px'}; const changeName = useCallback(() => { console.log('测试给子组件传递方法,使用useCallback后,子组件是否还会进行无效渲染'); },[]) return ( <> <button onClick={(e) => { setCount(count+1) }}>加1</button> <p>count:{count}</p> <FunMemo childStyle={childStyle} changeName={changeName} /> </> ) } export default Page;
使用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
37
38
39
40
41
42import React, { useState,memo, useMemo, useCallback} from 'react'; //子组件会有不必要渲染的例子 interface ChildProps { childStyle: { color: string; fontSize: string;}; changeName: ()=>void; } const FunChild = ({ childStyle,changeName}: ChildProps): JSX.Element => { console.log('普通函数子组件') return( <> <div style={childStyle}>我是普通函数子组件</div> <button onClick={changeName}>普通函数子组件按钮</button> </> ); } const FunMemo = memo(FunChild); const Page = (props:any) => { const [count, setCount] = useState(0); const [name, setName] = useState(""); const childStyle = {color:'green',fontSize:'16px'}; const changeName = useCallback(() => { setName('变一下名称') }, []) const childStyleMemo = useMemo(() => { return { color: name === '变一下名称' ? 'red':'green', fontSize: '16px' } }, [name]) return ( <> <button onClick={(e) => { setCount(count+1) }}>加1</button> <p>count:{count}</p> <FunMemo childStyle={childStyleMemo} changeName={changeName} /> </> ) } export default Page;
以上就是React Hooks使用避坑指南的详细内容,更多关于React Hooks使用的资料请关注靠谱客其它相关文章!
最后
以上就是活力汽车最近收集整理的关于React Hooks使用常见的坑的全部内容,更多相关React内容请搜索靠谱客的其他文章。
发表评论 取消回复