一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第5天,点击查看活动详情。
理论:
1、useMemo和useCallback的参数跟useEffect基本一致,他们之间最大的区别有是useEffect会用于处理副作用,而前两个hooks不能。
2、useMemo和useCallback都会在组件第一次渲染的时候执行,之后会在其依赖的变量发生改变时再次执行;并且这两个hooks都返回缓存的值,useMemo返回缓存的变量,useCallback返回缓存的函数。
我们先来看useMemo
import React,{useState} from 'react'
const Memo = () => {
const [count,setCount] = useState(1)
const [sum ,setSum] = useState(0)
function getCount (){
let p = 0
for (let index = 0; index <count* 100; index++) {
p+=index
}
console.log(p)
return p
}
return <div>
<h1>{count}===={sum}===={getCount()}</h1>
<button onClick={()=>setCount((count)=>count+1)}>count</button>
<button onClick={()=>setSum((sum)=>sum+1)}>sum</button>
</div>
};
export default Memo
这里我们希望点击count按钮count+1,getCount()函数的返回值随着变化
当我们点击了count按钮发现count加了1,函数的返回值也从新计算变化,控制台打印一次console.log(p),这个使我们希望的
可是当我们点击sum按钮,我们发现sum加了1,控制台又打印了一次console.log(p)
控制台又打印了一次,说明getCount()又走了一次,这是我们不希望看到的,因为我操作的sum,getCount()这个函数依赖的是count,我没有想让你走,你却走了,里面计算的逻辑白走一次,这就是对性能的浪费
为什么getCount函数会执行两次呢?
因为在函数组件中,react不区分mount和update两个状态,这意味着函数组件的每一次调用都会执行其内部的所有逻辑,这里依赖的是变量,所以我们要用useMemo这个hooks
接下来改造代码
const getCount = useMemo(()=>{
let p = 0
for (let index = 0; index <count* 100; index++) {
p+=index
}
console.log(p)
return p
},[count])
我们把逻辑放在useMemo的第一个参数里执行,然后数组的依赖项里,写上count,此时点击count按钮,发现count+1,打印走了一次,没有问题
这是当我们再次点击sum按钮时,我们发现这时sum+1,但是控制台已经没了打印,说明组件虽然更新了,但是useMemo这个函数却没有走,因为useMemo里的逻辑依赖的是count这个变量,并且会把它的状态缓存起来,如果count没有发生变化,useMemo里用的就是缓存的旧count,只有当count发生变化,useMemo才会被执行
useCallback和useMemo的差别不大,只不过useCallback是依赖的变量发生变化后返回的是新的函数,如果没有发生变化,就是用缓存的旧函数
//因为返回来的是新函数,函数不能展示在页面上,这里我们借用ES6的新增的数据类型Set来展示
import React,{useState,useCallback} from 'react'
const set = new Set() //这里set的声明要写在函数的外面,不然每次数据发生变化,set的值都为空,就看不出来结果
const Call = () => {
const [count,setCount] = useState(1)
const [sum ,setSum] = useState(0)
const getSum = useCallback(()=>{
console.log(count)
},[count])
set.add(getSum)
console.log(set)
return <div>
<h1>{count}===={sum}===={set.size}</h1>
<button onClick={()=>setCount((count)=>count+1)}>count</button>
<button onClick={()=>setSum((sum)=>sum+1)}>sum</button>
</div>
};
export default Call
这是我们点击count按钮,发现随着count加1,set.size也在加1,控制台里打印的set里的函数随着count变化,在不断增加新函数
此时点击sum按钮,虽然sum在加1,但是set.size的数量没有发生改变, 控制台里的输出set也没有增加新函数,说明虽然函数组件在更新,但是useCallback的依赖项count没有发生变化,useCallback就不会执行,一直用的是缓存的函数
useCallback的使用场景还是很多的,所有依赖本地状态或props来创建函数,需要使用到缓存函数的地方,都可以使用useCallback,但是要配合React.memo使用,不然就很有可能造成负优化
举例:
有一个父组件,其中包含子组件,子组件接收一个函数作为props;通常而言,如果父组件更新了,,子组件也会执行更新;但是大多数场景下,更新是没有必要的,我们可以借助useCallback和React.memo配合使用,然后把这个函数作为props传递给子组件;这样,子组件就能避免不必要的更新。
//在father.tsx中的代码
import React,{useState,useCallback} from 'react'
import Son from './Son'
const set = new Set()
const Father = () => {
const [count,setCount] = useState(1)
const [sum ,setSum] = useState(0)
const getSum = useCallback(()=>{
return count
},[count])
set.add(getSum)
console.log(set)
return <div>
<h1>{count}===={sum}===={set.size}</h1>
<button onClick={()=>setCount((count)=>count+1)}>count</button>
<button onClick={()=>setSum((sum)=>sum+1)}>sum</button>
<Son getSum={getSum}></Son> // 这里我们把每次count改变,返回的新函数传给子组件
</div>
};
export default Father
在Son.jsx中接收函数作为props
import React,{useState,useEffect} from 'react'
type Props = {
getSum:()=>number
}
const Son =React.memo(({getSum}:Props) => {
const [count,setCount] = useState(()=>getSum()) //子组件也定义个状态count,初始值为父组件传过来的函数的返回值
useEffect(()=>{
setCount(getSum())
},[getSum]) //将getSum传入useEffect的数组中,只要父组件那边传过来新的函数,useEffect监听到,就将子组件里的count的值设置为新函数的返回值
console.log('我是子组件')
return <div>
<h1>{count}</h1> //将count的值展示在页面上
</div>
})
export default Son
此时运行代码,我们点击父组件里的count按钮发现,随着父组件里的count增加,useCallback会返回新的函数,新的函数传给子组件,React.memo内部做对比发现函数发生了变化,子组件机会重新渲染更新,子组件useEffect监听到是新的函数,将新函数的返回值设置给子组件的count,这时页面上得父组件的count、set.size和子组件的count是同步的
这里控制台打印了两边我是子组件,是因为父组件一开始渲染走子组件走一遍,当父组件值发生变化,子组件又会走一遍
而点击父组件的sum按钮,sun的值在增加,但是其他的都没有变化
这样我们就通过useCallback和React.memo进行配合实现了性能的优化,将只有当useCallback里的依赖项发生变化,才会产生新的函数,子组件内部通过React.memo作比较,只有新函数子组件才跟着更新
注意点:
useEffect、useMemo、useCallback都是自带闭包的。也就是说,每一次组件的渲染,其都会捕获当前组件函数上下文中的状态(state, props),所以每一次这三种hooks的执行,反映的也都是当前的状态,你无法使用它们来捕获上一次的状态。对于这种情况,我们应该使用ref来访问。