10 useCallback基础用法
useCallback概念解释
我们第五个要学习的Hook(钩子函数)是useCallback,他的作用是“勾住”组件属性中某些处理函数,创建这些函数对应在react原型链上的变量引用。useCallback第2个参数是处理函数中的依赖变量,只有当依赖变量发生改变时才会重新修改并创建新的一份处理函数。
react原型链?
我对react原型链也不太懂,你可以简单得把 react原型链 理解成 “react定义的一块内存”。我们使用某些 hook 定义的“变量或函数”都存放在这块内存里。这块内存里保存的变量或函数,并不会因为组件重新渲染而消失。
1、当我们需要使用时可以“对象的引用名”从该内存里获取,例如useContext
2、当希望更改某些变量时,可以通过特定的函数来修改该内存中变量的值,例如useState中的setXxxx()
3、当某些函数依赖变量发生改变时,react可以重新生成、并修改该内存中对应的函数,例如useReducer、useCallback
此处更新与2020年10月13日
今天学习了一下 JS 原型链:每一个对象或者说由 function 创建的对象,他们都有一个属性__proto__
,该属性值为创建该对象的构造函数的原型对象,又称 隐式原型,而这一层的隐式原型也有__proto__
属性,即__proto__.__proto__
属性值为 Object.prototype,还可以继续再往下深入__proto__.__proto__.__proto__
为了避免死循环,最终到此,即Object.prototype.__proto__
为 null。作为构造函数对象,有属性 prototype,属性值为该函数的显示原型对象。constructor 则表示原型对象的构造函数本身。const arr = [1, 2, 3]
console.log(arr.__proto__ === Array.prototype) // true
console.log(arr.__proto__.__proto__ === Object.prototype) // true
console.log(Object.prototype.__proto__ === null) // true
function MyFun() { this.name = 'puxiao' }
const myFun = new MyFun()
console.log(myFun.__proto__ === MyFun.prototype) // true
console.log(MyFun.__proto__ === Function.prototype) // true
console.log(myFun.__proto__.__proto__ === Object.prototype) // true
console.log(myFun.__proto__.__proto__.__proto__) // null
console.log(Object.prototype.__proto__) // null
要想更加容易理解上面代码,就需要明白,所谓 object 是指 ,而 Object 其实是 JS 内置的对象函数。同理 所谓 array 是指 [],而 Array 其实是 JS 内置的 数组函数
正是 JS 中
__prototype__(隐式原型)
prototype(显式原型)
constructor(原型对象所在的构造函数本身)
这 3个概念,最终组合成了 庞大的 JS 功能。我们平时定义的任何 类、对象、函数 都出在这种 链条 中,以及对 这个链条中某个环节属性功能的扩展,这种组织形式就叫 JS 原型链。
JS 原型还有一个原则就是可以无限得去扩展自身属性,当前级别的原型扩展属性之后,下层级别的对象自动就拥有该属性。
那么我们可以大概推理出来,React就是巧妙利用了这种 JS 原型链的原则,将底层模块需要用到的处理函数提升到更高(或者说更加原始)的级别中。这样即使底层中发生了变化,但是他依然拥有高层中定义好的函数引用。
请重点留意“修改”这个词,因为“修改”牵扯到react最为隐秘却极其重要的一层概念。
“修改”有3种情况:
1、用完全不一样的新值去替换之前的旧值 ——> 这会触发react重新渲染 ——> 例如{age:34}
去替换{age:18}
2、用和旧值看似一模一样的新值去替换之前的旧值 ——> 这依然会触发react重新渲染,因为react底层对新旧值做对比时使用的是 Object.is判断,字面上看似一模一样没有用,react依然会认为这是2个对象,依然会触发react重新渲染 ——> 例如{age:18}
去替换{age:18}
3、用旧值的引用去替换旧值 ——> 这次就不会触发重新渲染 ——> 例如let obj={age:18}; let obj2=obj
,用obj2去替换obj
为了提高react性能,就需要用旧值的引用去替换旧值,从而阻止本次无谓的渲染。
问题的关键就在于“如何获取旧值的引用”?
答:对于函数来说可以使用useCallback。
在本章或以后的章节中,我依然会使用 react原型链 这个词,你都按照我刚才说的“react定义的一块内存”概念去理解就好了。
懵圈了没?我已经尽量总结得让你容易理解了,如果你似懂非懂没有关系,本文后面会通过示例代码会让你明白如何使用。
让我们先忘掉useCallback,先来学习一下以下2个知识点。
第1个知识点:React.memo() 的用法
首先我们知道,默认情况下如果父组件重新渲染,那么该父组件下的所有子组件都会随着父级的重新渲染而重新渲染。
1、无论子组件是类组件或是函数组件。
2、无论子组件在本次渲染过程中,子组件是否有任何相关的数据变化。
举例,假设某父组件中有3个子组件:子组件A、子组件B、子组件C。若因为子组件A发生了某些操作,引发父组件重新渲染,这时即使子组件B和子组件C没有任何需要更改的地方,但是默认他们两个也会重新被渲染一次。
为了减少这个不必要的重新渲染,如果是类组件,可以在组件shouldComponentUpdate(准备要开始更新前)生命周期函数中,通过比较props和state中前后两次的值,如果完全相等则跳过本次渲染,改为直接使用上一次渲染结果,以此提高性能提升。
伪代码如下:
shouldComponentUpdate(nextProps,nextStates){
//判断xxx值是否相同,如果相同则不进行重新渲染
return (nextProps.xxx !== this.props.xxx); //注意是 !== 而不是 !=
}
为了简化我们这一步操作,可以将类组件由默认继承自React.Component改为React.PureComponent。React.PureComponent默认会帮我们完成上面的浅层对比,以跳过本次重新渲染。
请注意:React.PureComponent会对props上所有可枚举属性做一遍浅层对比。而不像 shouldComponentUpdate中可以有针对性的只对某属性做对比。
上面讲的都是类组件,与之对应的是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点:
1、React.memo()只会帮我们做浅层对比,例如props.name='puxiao'
或props.list=[1,2,3]
,如果是props中包含复杂的数据结构,例如props.obj.list=[{age:34}]
,那么有可能达不到你的预期,因为不会做到深层次对比。
2、使用React.memo仅仅是让该函数组件具备了可以跳过本次渲染的基础,若组件在使用的时候属性值中有某些处理函数,那么还需要配合useCallback才可以做到跳过本次重新渲染。
呵,话题又回到useCallback上面了。
第2个知识点:=== 等比运算
在原生JS中,你认为
1、{}==={}
为true还是false?
2、{a:2}==={a:2}
为true还是false?
这是一道很简单却很容易迷惑人的题目,若你对原生JS中 === 等比运算不够深入了解,你很容易会认为结果是true。
如果你轻松回答出来:以上均为false,那么恭喜你是个明白人。
如果你疑惑了一下或者你的答案是true,那么你可以自己去JS里测试一下看结果是什么。
答案是2者均是false。
以===为例,虽然从字面上 === 左右两侧完全相同的,但是实际上在JS中 左右两侧分别为独立的对象,各自占 有各自的内存空间,因此他们对比的结果是false。
相反,看下面的代码:
let obj = {};
let obj2 = obj;
obj2.name='react';
console.log(obj===obj2); //true
上面输出结果为true,为何obj===obj2为true? 因为 obj和obj2都是对同一个对象的引用,所以对比结果为true,因为他们最终指向同一个对象。
还记得本文开头对于useCallback概念解释中的那段文字吗?useCallback的作用是“勾住”组件属性中某些处理函数,创建这些函数对应在react原型链上的变量引用。
呵,话题又回到useCallback上面了。
划重点:记住“useCallback”和“原型链上处理函数的引用”这两个关键词,基本上你就对useCallback的原理理解一大半了。
让我们回到useCallback基础学习中。
useCallback是来解决什么问题的?
答:useCallback是通过获取函数在react原型链上的引用,当即将重新渲染时,用旧值的引用去替换旧值,配合React.memo,达到“阻止组件不必要的重新渲染”。
详细解释:
useCallback可以将组件的某些处理函数挂载到react底层原型链上,并返回该处理函数的引用,当组件每次即将要重新渲染时,确保props中该处理函数为同一函数(因为是同一对象引用,所以===运算结果一定为true),跳过本次无意义的重新渲染,达到提高组件性能的目的。当然前提是该组件在导出时使用了React.memo()。
补充说明:
假设某组件使用到了myfun这个处理函数,回忆一下前面提到的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){
//跳过本次重新渲染,改为使用上一次渲染结果即可
}
由于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){
//跳过本次重新渲染,改为使用上一次渲染结果即可
}
此时 obj.myfun 和 obj2.myfun 均为myfun的引用,因此该对比结果为true,也就意味着可以顺利跳过本次渲染,达到提高组件性能的目的。
以上是代码仅仅是为了示意默认子组件为什么会被迫重新渲染,以及useCallback作用机理。
只有理解了这个机理,才会明白何时使用useCallback。 切记不要滥用useCallback。
多说一句:你是否觉得React Hook 很绕? 对,这就是Hook学习起来难度大的一些原因,但当你充分理解React的编程哲学思想后,用起来会如鱼得水。加油!
useCallback函数源码:
回到useCallback的学习中,首先看一下React源码中的ReactHooks.js。
//备注:源码采用TypeScript编写,如果不懂TS代码,阅读起来稍显困难
export function useCallback<T>(
callback: T,
deps: Array<mixed> | void | null,
): T {
const dispatcher = resolveDispatcher();
return dispatcher.useCallback(callback, deps);
}
上述代码看不懂没关系,本系列教程只是讲述“如何使用Hook”,并不是“Hook源码分析”。^_^
不过请注意第2个参数,deps为该函数依赖的数据变量,值为Array<mixed>
或 void 或 null。 意味着如果该函数没有依赖的情况下,可以传入空数组[]或void或null。个人建议是传入空数组。
补充一点TypeScript知识(因为我最近刚学了TypeScript):
像 <T\>(callback:T):T
这种类型定义称为“泛型”,里面 T 的含义为“一模一样的同类型”。
举例:
1、若T为function,即参数callback类型为function,那么函数返回值也为function。
2、若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个参数有以下这段话:
注意:依赖项数组不会作为参数传给回调函数。虽然从概念上来说它表现为:所有回调函数中引用的值都应该出现在依赖项数组中。未来编译器会更加智能,届时自动创建数组将成为可能。
自己体会吧,你品,你细品。