useCallBack()

  • 我们都知道,在函数式组件中使用useState中的修改方法会将整个组件重新渲染一遍,但是如果组件中存在其他内部函数的话,那么该函数就会无可避免的重新再执行一遍,也就是说,函数内部的临时变量也会无可避免的重新赋值, 但是我们反观useState中的状态, 我们在useState中定义的状态是能够被React给记住的(状态变化页面改变),那么我们能不能让React也记住我们的组件内部的函数(设置一个依赖来限制函数的重新执行,依赖改变,函数重新创建)呢? useCallBack()就能实现这样的作用!

函数式组件中,使用useCallback对函数进行缓存(被外层函数包裹,相当于闭包),组件再次更新时(函数重新执行)会根据依赖是否变化决定选用缓存函数【之前生成的函数】还是新函数【新生成的上下文】。
一般会在嵌套组件中,与函数式组件的memo和类组件的PureComponent一起使用【会对传入props参数逐个进行浅比较决定是否需要更新】,来提高页面性能。

参数解析:

useCallback接受两个参数,第一个参数是:函数(我们在组件内部填写的函数),是箭头函数还是普通函数都没听有影响; 第二个参数是监视依赖,它决定组件更新是否生成新函数,函数内部使用到的依赖尽可能在第二个参数写全,否则会有意想不到的问题

何时生成新函数 第二个参数
组件首次执行及更新都会生成新函数
组件首次执行生成,之后不变 []
组件首次执行、依赖变化时生成新函数 [state, ref.current] 这两类

案例展示:

  • 这个案例展示三种函数在函数式组件内部调用的过程
    1. 监视age属性: 使用useCallback监视age属性,结果就是每一次age属性变化,该函数都会重新执行,获取最新的age值
    2. 使用usaCallback不监视任何属性但输出name状态,结果就是因为useCallback内部没有设置监视依赖,因此输出的name永远是旧的值(初始值一直不吃不变)
    3. **不使用useCallback,结果就是这个函数在组件内部就是一个普通函数,组件的每一次更新都会重新调用这个函数,因此这个函数里面的值永远保持变化(内部参数每一次调用都会初始化)
// useCallBack能够将我们的组件内部的函数记忆下来,使用依赖去限制函数重新生成和执行
/*
函数式组件中的内部函数每一次组件更新都会重新创建和执行,也就是说里面的参数都会重新初始化
赋值,这对于整个项目来说是是浪费资源的表现,尽管js创建一个函数的成本是非常小的,由此衍生出
一个新的钩子(hooks)useCallback,在函数式组件中,使用useCallback可以对组件内部的函数进行
缓存(被外层函数包裹,相当于闭包),组件再次更新时(函数重新执行)会根据依赖是否变化决定选用缓存函数
还是新的函数
*/

import React,{useCallback,useState} from 'react'

export default function App() {
const [name,setName] = useState('lam')//姓名
const [age,setAge] = useState(19)//姓名

// 定义修改姓名提示函数
const ageChanged = useCallback(()=>{
console.log('年龄改变了(监视age属性,每当age属性更新,函数重新执行)!',age);
},[age])//监视name依赖

// 定义修改年龄提示函数
const nameChanged = useCallback(()=>{
console.log('我没有设置监视依赖,因此这个函数没有重新生成,因此里面的变量还是旧变量!',name);
},[])//不监视任何依赖

// 普通函数
const say = ()=>{
var a = 0
console.log(`
页面重新渲染!
(这是一个普通函数,每一次状态改变都会驱使页面重新刷新,组件内部的函数重新创建执行)
`,a);
return ++a
}

return (
<div>
a的值为:{say()}
<p>姓名:{name}-{age}</p>
<button onClick={()=>{
setName('肥林')
nameChanged()//修改姓名时执行修改姓名函数提示
}}>修改名字</button>
<button onClick={()=>{
setAge(age+1)
ageChanged()//修改年龄时执行修改年龄函数提示
}}>修改年龄</button>
</div>
)
}

结果展示:

image

useMemo

  • 官方文档

  • 我们可以简单的认为useMemo就是Vue中的计算属性,在React中,useMemo可以完全替代useCallback,也就是说如果你想通过使用 useMemo 返回一个记忆函数也是完全可以的。

  • 使用方法与上面的基本一致,只不过不同的是useMemo的第一个参数是箭头函数且需要有返回值,返回值计位我们想要缓存的函数体

  • *唯一的区别是:useCallback*不会执行第一个参数函数,而是将它返回给你, useMemo会执行第一个函数并且将函数执行结果返回给你。

  • 所以 useCallback 常用记忆事件函数,生成记忆后的事件函数并传递给子组件使用。而 useMemo 更适合经过函数计算得到一个确定的值,比如记忆组件。

案例展示:

// useMemo能够将我们的组件内部的函数记忆下来,使用依赖去限制函数重新生成和执行

import React,{useMemo,useState} from 'react'

export default function App() {
const [name,setName] = useState('lam')//姓名
const [age,setAge] = useState(19)//姓名

// 定义修改姓名提示函数
const ageChanged = useMemo(()=>{
// 通过返回值返回函数体
return ()=>{console.log('年龄改变了(监视age属性,每当age属性更新,函数重新执行)!',age);}
},[age])//监视name依赖

// 定义修改年龄提示函数
const nameChanged = useMemo(()=>{
// 通过返回值返回函数体
return ()=>{console.log('我没有设置监视依赖,因此这个函数没有重新生成,因此里面的变量还是旧变量!',name);}
},[])//不监视任何依赖

// 普通函数
const say = ()=>{
var a = 0
console.log(`
页面重新渲染!
(这是一个普通函数,每一次状态改变都会驱使页面重新刷新,组件内部的函数重新创建执行)
`,a);
return ++a
}

return (
<div>
a的值为:{say()}
<p>姓名:{name}-{age}</p>
<button onClick={()=>{
setName('肥林')
nameChanged()//修改姓名时执行修改姓名函数提示
}}>修改名字</button>
<button onClick={()=>{
setAge(age+1)
ageChanged()//修改年龄时执行修改年龄函数提示
}}>修改年龄</button>
</div>
)
}

结果展示:

image

memo

  • 官方文档,可以认为是类组件中的pureComonents
  • memo用来优化函数组件重复渲染行为,针对的是一个组件,而useMemo返回一个memoized(被记忆缓存)的,本质都是用同样的算法来判定依赖是否发生改变,继而决定是否触发memo或者useMemo中的逻辑,利用memo就可以避免不必要的重复计算,减少资源浪费; 而useCallback则是返回一个被记忆缓存的函数;
  • useMemouseCallback都接收两个参数,第一个参数为fn,第二个参数为[],数组中是变化依赖的参数, memo则可以直接作用于组件

案例展示:

  • 设置父子组件(父组件包裹子组件),每一次点击父组件,num对应+1,但是子组件不会重新渲染
import React,{useState,memo} from 'react'

// 父组件
export default function Parent(){
const [count, setCount] = useState(1)
const [flag, setFlag] = useState('标志')
const addCount = ()=>{
setCount(count + 1)
}
return(
<>
<p>--------------父组件--------------</p>
{/* 每次点击父组件中的count+1,但是子组件中的props或者states没有变化 */}
<div onClick={addCount}>计数: {count}</div>
<Child flag={flag}/>
</>
)
}

// 子组件(使用memo)
const Child = memo((props)=>{
console.log('子组件重新渲染!',props)
return(
<div>--------------子组件--------------</div>
)
})

// 使用memo后,子组件的props或者state没有变化时,将不会对子组件进行渲染,因此子组件只会输出一次

结果展示:

image

memo结合useCallbackuseMemo来优化子组件的更新操作

  • 我们通常使用memo来解决父子间通信(父=>子)传值导致子组件频繁重新渲染的问题,以及使用memo结合useMemo或者useCallback来解决子传父之间通信导致子组件频繁渲染的问题,父传子的优化直接使用memo即可,代码层面可观察上面memo的代码展示,下面展示子传父的优化方案:

分以下两种情况:

  1. 父子通信间(子=>父)的嵌套,没有使用useCallbackuseMemo,外层组件每次更新相当于重新生成新函数(ageChanged),传给子组件,子组件通过memo包裹对传入props参数进行浅比较,发现onClick触发的函数*有变化*,进而子组件更新(即每一次点击按钮给父组件修改数据,子组件都重新渲染)。这种方法不推荐使用
import React,{useState,memo} from 'react'

// 子组件(使用memo包裹)
const Son = memo((props) => {
console.log('子组件重新渲染!');
return (
<div>
<p>------------子组件--------------------</p>
<button onClick={()=>
props.ageChanged()//修改年龄时执行修改年龄函数提示
}>修改年龄</button>
</div>
)
})

// 父组件(使用useCallback或者useMemo)来传递函数给子组件
export default function App() {
const [age,setAge] = useState(19)//姓名

// 1. 不使用 useCallback 或 useMemo 定义更改年龄方法
const ageChanged = ()=>{
setAge(age=>age+1)
}

return (
<div>
---------------父组件---------------
<p>num:{age}</p>
{/* 通过props传值 */}
<Son ageChanged={ageChanged}></Son>
</div>
)
}
  • 结果展示:
  • image

上图可以看出,每一次点击按钮(子组件向父组件传值,父组件重新渲染),子组件都会重新渲染,频繁渲染的问题没有得到优化!

  1. 父子通信间(子=>父)的嵌套使用useCallbackuseMemo,且第二参数为[],也就是说该函数不监视任何依赖,即外层组件每次更新时(暂时不考虑依赖)都会复用上次函数。子组件接收到传入propsageChanged没有变化,所以子组件不会发生更新。这种方法推荐使用
// 07-memo结合useCallback和useMemo来使用,优化子组件的频繁更新
/*
我们有时候想要通过父子传值的方式实现子组件调用父组件的属性,但是子组件被父组件包裹
也就是说每一次父组件更新的时候子组件重新渲染无可避免的子组件也会重新渲染,如果此时子组件
里面又有非常多的嵌套组件又或者逻辑层次的话,子组件的重新渲染会非常的慢,导致项目的运行效率
不高,此时我们可以使用memo结合useCallback或者useMemo来实现优化这种情况!

实现原理: 父组件以useCallback或者useMemo设置一个操作state的方法,依赖填空数组[](即不监视任何属性),
即每一次外层组件更新时都会复用上一次的方法并通过props将这个方法传给子组件,子组件使用memo包裹,
使用memo包裹后,子组件发现props每一次传过来的方法都是上一次的方法(即props的值没有发生变化)
那么在子组件组件中调用这个方法实现操作父组件的状态,父组件状态更新,子组件却不会再更新了!
*/

import React,{useCallback,useMemo,useState,memo} from 'react'

// 子组件(使用memo包裹)
const Son = memo((props) => {
console.log('子组件重新渲染!');
return (
<div>
<p>------------子组件--------------------</p>
<button onClick={()=>
props.ageChanged()//修改年龄时执行修改年龄函数提示
}>修改年龄</button>
</div>
)
})

// 父组件(使用useCallback或者useMemo)来传递函数给子组件
export default function App() {
const [age,setAge] = useState(19)//姓名

/* ---------------------------------------------------- */
// 1. 使用 useCallback 定义更改年龄方法且不监视任何依赖
// const ageChanged = useCallback(()=>{
// setAge(age=>age+1)
// },[])//监视name依赖,每一次组件更新,缓存ageChanged函数

/* ----------------------------------------------------- */
// 2. 使用 useMemo 定义更改年龄方法且不监视任何依赖
const ageChanged = useMemo(()=>{
/*
切记要使用age=>age+1,不能使用age+1,因为它会带来问题:
因为这里没有监视age依赖,也就是说,这个缓存函数不受age控制,
每一次age变化,缓存函数中的age永远是19+1,也就导致了页面上的
age展示永远是20,但是使用箭头函数就不一样了.使用箭头函数的话
通过返回值的形式age=>age+1 可看作为 age=19+1,age=(19+1)+1,
这样就能实现!
*/
return ()=>setAge(age=>age+1)
},[])//监视name依赖,每一次组件更新,缓存ageChanged函数

return (
<div>
---------------父组件---------------
<p>num:{age}</p>
{/* 通过props传值 */}
<Son ageChanged={ageChanged}></Son>
</div>
)
}
  • 结果展示:

image

上图可以看出,每一次点击按钮(子组件向父组件传值,父组件重新渲染),子组件都不会重新渲染,频繁渲染的问题得到优化!