useCallback基础用法
React Hook useCallback基础用法
第五个Hook(钩子函数)是useCallback,他的作用是"钩住"组件属性中某些处理函数,创建这些函数对应在react原型链上的变量引用。useCallback第2个参数是处理函数中的依赖变量,只有当依赖变量发生改变时才会重新修改并创建的一份处理函数。
react原型链?
我对react原型链也不懂,但是可以简单的把react原型链 理解成"react定义的一块内存"。使用某些 hook 定义的"变量或函数"都存放在这块内存里。这块内存里保存的变量或函数,并不会因为组件重新渲染而小时。
- 当需要使用可以"对象的引用名"从该内存里获取,例如useContext
- 当希望更改某些变量时,可以通过特定的函数来修改该内存中变量的值,例如useState中的setXxx()
- 当某些函数依赖变量发生改变时,react可以重新生成、并修改该内存中对应的函数,例如useReducer、useCallback
这里这个"修改"这个词,因为"修改"牵扯到react最为隐秘极其重要的一层概念。
"修改"有3种情况:
- 用完全不一样的新值去替换之前的旧值 ——> 这会触发react重新渲染 ——> 例如{age: 34}去替换{age: 18}
- 用和旧值看似一模一样的新值去替换之前的旧值 ——> 这依然会触发react重新渲染,因为react底层对新旧值做比对时使用的是 Object.is 判断,字面上看似一模一样没有用,react依然会认为这是2个对象,依然会出发react重新渲染 ——> 例如{age:18}去替换{age:18}
- 用旧值的引用去替换旧值 ——> 这次就不会触发重新渲染 ——> 例如:let obj = {age: 18}; let obj2 = obj,用obj2去替换obj
为了提高react性能,就需要用旧值的引用去替换旧值,从而阻止本次没用的渲染。
问题的关键来了,如何获取旧值的引用?
答:对于函数来说可以使用useCallback就可以。
ok,先学习以下2个知识点。useCallback一会儿说。
第1个知识点:React.memo() 的用法
首先要知道,默认情况下如果父组件重新渲染,那么该父组件下的所有子组件都会随着父级的重新渲染而重新渲染。
- 无论子组件是类组件还是函数组件。
- 无论子组件在本次渲染过程中,子组件是否有任何相关的数据变化。
假设某父组件中有3个子组件:子组件A、子组件B、子组件C。若因为子组件A发生了某些操作,引发父组件重新渲染,这时即使子组件B和子组件C没有任何需要更改的地方,但是默认他们两个也会重新被渲染一次。
为了减少这个不必要的重新渲染,如果是类组件的话,可以在组件shouldComponentUpdata(准别要开始更新前)生命周期中,通过比较props和state中前后两次的值,如果完全相等则跳过本次渲染,改为直接使用上一次渲染结果,以此提高性能提升。
伪代码:
shouldComponentUpdata(nextProps, nextStates) {
// 判断xxx值是否相同,如果相同则不进行重新渲染
return (nextProps.xxx !== this.props.xxx); // 注意是 !== 而不是 !=
}
2
3
4
为了简化这一步操作,可以将类组件由默认继承自React.Component改为React.PureComponent。React.PureComponent默认会完成上面的浅层对比,以跳过本次重新渲染。
注意下:React.PureComponent会对props上所有可枚举属性做一遍浅层对比。而不像 shouldComponentUdate中可以有针对性的指对某属性做对比。
上面说的都是类组件,与之对应的是React.memo(),这个是针对函数组件的,作用和React.PureComponent完全相同。
React.memo()的使用方法很简单,就是把要导出的函数组件包裹在React.memo中即可。
伪代码:
import React from 'react';
function Xxxx() {
return <div>xx</div>;
}
export default React.memo(Xxxx); // 使用React.memo包裹住导出的函数组件
2
3
4
5
但是要记住以下2点:
- React.memo()只会帮我们做浅层对比,例如props.name = 'liam' 或 props.list = [1, 2, 3],如果是props中包含复杂的数据结构,例如props.obj.list = [{age: 18}],那么就达不到预期,因为不会做到深层次的比对。
- 使用React.memo仅仅是让该函数组件具备了可以跳过本次渲染的基础,若组件在使用的时候属性值中有某些处理函数,那么还需要配合useCallback才可以做到跳过本次重新渲染。
...,不是在说memo么,咋又说到了useCallback。~~~
第2个知识点:=== 等比运算
在原生JS中
- {} === {} 为true还是false?
- {a: 2} === {a: 2} 为true还是false?
这是一道很容易却很容易迷惑人的题目,如果对原生ja中 === 等比运算不够深入了解,很容易会认为结果为true。
答案两者均是false。
以{}==={}为例,虽然字面上 === 左右两侧完全相同的,但是实际上在js中 左右两侧分别为独立的{}对象,各自占有各自的内存空间,因此他们的对比结果都是false。
相反,看下面的代码:
let obj = {};
let obj1 = obj;
obj2.name = 'react';
console.log(obj === obj2); // true
2
3
4
上面输出结果为true,为何obj === obj2为true?因为obj和obj2都是对同一个对象的引用,所以对比结果为true,因为他们最终指向同一个对象。
还记得开头对于useCallback概念解释中的那字?useCallback的作用是"钩住"组件属性中某些处理函数,创建这些函数对应在react原型链上的变量引用。
重点!圈起来要考的:记住"useCallback"和"原型链上处理函数的引用"这两个关键词。
来回到useCallback基础学习中。
# useCallback是来解决什么问题的?
useCallback是通过获取函数在react原型链上的引用,当即将重新渲染时,用旧值的引用去替换旧值,配个React.memo,达到"阻止组件不必要的重新渲染"。
# 详细解释
useCallback可以将组件的某些处理函数挂载到react底层原型链上,并返回该处理函数的引用,当组件每次即将要重新渲染时,确保props中该处理函数为同一函数(因为是同一个对象引用,所以===运算结果一定为true),跳过本次无意义的重新渲染,达到提高组件性能的目的。当前前提是该组件在导出时使用了React.memo()。
# 补充说明
假设某组件使用到了mufun这个处理函数,结合一下前面提到的js中 === 运算规则。
默认不使用useCallback,其实组件执行了以下伪代码:
let obj = {}; // 上一次渲染时创建的props
obj.myfun = {xxx}; // props中的myfun属性值,实为独立创建的{xxx}
let obj2 = {}; // 本次渲染时创建的props
obj2.myfun = {xxx}; // props中的myfun属性值,实为独立创建的{xxx}
if(obj.myfun === obj2.myfun) {
// 跳过本次渲染,改为使用上一次渲染结果即可
}
2
3
4
5
6
7
8
9
由于obj.myfun 和 obj2.myfun 为分别独立创建的函数{xxx},所以对比结果为false,也就意味着无法跳过本次重新渲染,尽管函数{xxx}字面相同。
相反,如果使用了useCallback,其实组件执行了以下伪代码:
let myfun = {xxx}; // 独立定义处理函数myfun
let obj = {}; // 上一次渲染时创建的props
obj.myfun = myfun; // props中的myfun属性值,实为myfun的引用
let obj2 = {}; // 本次渲染时创建的props
obj2.myfun = myfun; // props中的myfun属性之,实为myfun的引用
if(obj.myfun === obj2.myfun) {
// 跳过本次渲染,改为使用上一次渲染结果即可
}
2
3
4
5
6
7
8
9
10
11
此时 obj.myfun 和 obj2.myfun 均为myfun的引用,因此该比对结果为true,也就意味着可以顺利跳过本次渲染,达到提高组件性能的目的。
以上是代码仅仅是为了示意默认子组件为什么会被迫重新渲染,以及useCallback作用机理。
只有理解了这个机理,才会明白何时使用useCallback。切记切记,不要滥用useCallback。
多说一句,我写的时候我都觉的 React Hook 很绕,我又重新去官网看useCallback,哈哈,但是当充分理解React的编程哲学思想后,用起来就会如鱼得水。加油!!!
# useCallback函数源码
回到useCallback的学习中,首先看一下React源码中的ReactHooks.js (opens new window)
export function useCallback<T>(
callback: T,
deps: Array<mixed> | void | null,
): T {
const dispatcher = resolveDispatcher();
return dispatcher.useCallback(callback, deps);
}
2
3
4
5
6
7
要注意第2个参数,deps为该函数依赖的数据变量,值为Array 或 void 或 null。意味着如果该函数没有依赖的情况下,可以传入空数组[]或void或null。个人建议是传入空数组[]。
补充一点Typescript知识点。
像<T>(callback: T):T 这种类型定义成为"泛型",里面T的含义为"一模一样的同类型"。
举例:
- 若T为function,即参数callback类型为function,那么函数返回值也为function。
- 若T为object,即参数callback类型为object,那么函数返回值也为object。
# useCallback基本用法
useCallback(callback, deps)函数通常传入2个参数,第1个参数为我们定义的一个"处理函数",通常为一个箭头函数。第2个参数为该处理函数中存在的依赖变量,凡是处理函数中有的数据变量都需要放入deps中。如果处理函数没有任何依赖变量,可以传入一个空数组[]。
useCallback中的第2个依赖变量数组和useEffect中第2个依赖变量数组,作用完全不同!!!
useEffect中第2个依赖变量数组是真正起作用的,是具有关键性质的。而useCallback中第2个依赖变量数组目前作用来说仅仅是起到一个辅助作用。
仅仅是辅助?辅助了什么?既然处理函数中所有的依赖变量都需要作为第2个参数的内容,为啥React不智能一些,不用传第2个参数,省略掉这一步呢?
在React文章中,针对第2个参数有以下这段话
注意:依赖项数组不会作为参数传给回调函数。虽然从概念上来说它表现为:所有回调函数中引用的值都应该出现在依赖项数组中。未来编译器会更加智能,届时自动创建数组将成为可能
# 代码形式
import Button from './button'; // 引入一个自定义的组件<Button>
// 组件内声明一个age变量
const [age, setAge] = useStae(18);
// 通过callback,将鼠标点击处理函数保存到React底层原型链中,并获取该函数的引用,将引用赋值给handleClick
const handleClick = useCallback(() => {
setAge(age + 1)
}, [age]);
// 由于该处理函数中使用到了age这个变量,因此useCallback的第2个参数中,需要将age添加进去
// 使用该处理函数,实为使用该处理函数的在React底层原型链上的引用
return <Button handleClick={handleClick}></Button>
2
3
4
5
6
7
8
9
10
11
12
13
14
# 拆解说明
- 具体已经在示例代码中做了多项注释,不再重复。(不想浪费时间)
'age'补充说明
- 上述代码示例中,age为该组件通过useState创建的内部变量,事实上也可以是父组件通过属性传值的props.xx中的变量。
- 只要依赖变量不发生变化,那么重新渲染时就可以一直使用之前创建的那个函数,达到阻止本次渲染,提升性能的目的。但是如果依赖变量发生变化,那么下次重新渲染时根据变量重新创建一份处理函数并替换React底层原型链上原有的处理函数。
'handleClick'补充说明
再次强调,handleClick实际上是真正的处理函数在React底层原型链上的引用。
'<Button>'补充说明
<Button>为自定义的一个组件,在上述代码中相当于"子组件"。
上面示例伪代码仅仅是为了演示useCallback的使用方法,实际组件运用中,如果父组件只有1个子组件,那其实完全没有必要使用useCallback。只有父组件同时有多个子组件时,才有必要去做性能优化,防止某一个子组件引发的重新渲染也导致其他子组件跟着重新渲染。
# useCallback使用示例
若我们现在又一个自定义组件<Button>,代码如下
import React from 'react';
function Button({label, handleClick}) {
// 为了方便查看该子组件是否被重新渲染,这里增加一行console.log
console.log(`rendering ... ${label}`);
return <button onClick={handleClick}>{label}</button>
}
export default React.memo(Button); // 使用React.memo()包裹住要导出的组件
2
3
4
5
6
7
现在,要实现一个组件,功能如下:
- 组件内部有2个变量age,salary
- 有2个自定义组件Button,点击之后分别可以修改age,salary的值
若不使用useCallback,代码示例如下:
import React,{ useState, useCallback, useEffect } from 'react'
import Button from './Button';
function Mybutton() {
const [age, setAge] = useState(18);
const [salary, setSalary] = useState(7000);
useEffect(() => {
document.title = `Hooks - ${Math.floor(Math.random() * 100)}`
});
const handleClick01 = () => {
setAge(age + 1);
};
const handleClick02 = () => {
setSalary(salary + 1);
};
return (
<div>
{age} - {salary}
<Button label="Bth01" handleClick={handleClick01}></Button>
<Button label="Bth02" handleClick={handleClick02}></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
运行中无论点击哪个按钮,都会收到:
rendering ... Bt01
rendering ... Bt02
只是点击操作了其中一个按钮,另外一个按钮也要跟着重新渲染一次,试想一下如果该组件中有100个子组件甚至更多的话都要跟着重新渲染,那性能...
再来看看使用useCallback,示例代码如下:
import React, { useState, useEffect, useCallback } from 'react';
import Button from './Button';
function Mybutton() {
const [age, setAge] = useState(18);
const [salary, setSalary] = useState(7000);
useEffect(() => {
document.title = `Hooks -${Math.floor(Math.random()* 100)}`;
});
//使用useCallback()包裹住原来的处理函数
const handleClick01 = useCallback(() => {
setAge(age + 1);
}, [age]);
//使用useCallback()包裹住原来的处理函数
const handleClick02 = useCallback(() => {
setSalary(salary + 1);
}, [salary]);
return (
<div>
{age} - {salary}
<Button label='Bt01' handleClick={handleClick01}></Button>
<Button label='Bt02' handleClick={handleClick02}></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
修改后的代码,运行就会发现,当点击某个按钮时,仅仅是当前按钮重新做了一份渲染,另外一个按钮没有重新渲染,而是直接使用上一次的渲染结果。
使用useCallback减少子组件没有必要的渲染目的达成。
useCallback用法很简单,就是包裹住原来的处理函数,关键点得理解useCallback背后的机理,才能知道在什么情况下可以使用useCallback。否则很容易滥用 useCallback,反而造成性能的浪费。
# 思考题
假设上面示例代码中,做以下修改:每个按钮上新增一个属性:random={Math.floor(Math.random() * 100)}
<Button label='Bt01' handleClick={handleClick01}></Button>
<Button label='Bt02' handleClick={handleClick02}></Button>
// 修改为
<Button label='Bt01' handleClick={handleClick01} random={Math.floor(Math.random()*100)}></Button>
<Button label='Bt02' handleClick={handleClick02} random={Math.floor(Math.random()*100)}></Button>
2
3
4
5
此时这种情况针对性能优化而使用的useCallback没有任何意义,虽然使用useCallback保证了每次handleClick是相同的,可是 random 的值每次都是却是随机不一样的,尽管子组件<Button>并没有使用 random 这个值,但是它的加入造成了 props 每次都不一样(其实props.random 不一样),结果就是子组件每一次都会被重新渲染。所以此时使用useCallback已经失去了存在的意义。
至此,就已经写完了,高级用法??? 我也没遇到过useCallback高级用法... 要有了我遇到了学习完我再补。