useEffect , useMemo , useCallback
useEffect 、 useMemo 、 useCallback 对于回调,对于数据,对于函数 useEffect 、 useMemo 、 useCallback 都是自带闭包的。也就是说,每一次组件的渲染,其都会捕获当前组件函数上下文中的状态 (state , props) ,所以每一次这三种 hooks 的执行,反映的也都是当前的状态,你无法使用它们来捕获上一次的状态。对于这种情况,我们应该使用 ref 来访问
回顾
在介绍一下这两个 hooks 的作用之前,我们先来回顾一下 react 中的性能优化。在 hooks 诞生之前,如果组件包含内部 state ,我们都是基于 class 的形式来创建组件。当时我们也知道, react 中,性能的优化点在于
1. 调用 setState ,就会触发组件的重新渲染,无论前后的 state 是否不同
2. 父组件更新,子组件也会自动的更新
基于上面的两点,我们通常的解决方案是:使用 immutable 进行比较,在不相等的时候调用 setState ;在 shouldComponentUpdate 中判断前后的 props 和 state ,如果没有变化,则返回 false 来阻止更新。 在 hooks 出来之后,我们能够使用 function 的形式来创建包含内部 state 的组件。但是,使用 function 的形式,失去了上面的 shouldComponentUpdate ,我们无法通过判断前后状态来决定是否更新。而且,在函数组件中, react 不再区分 mount 和 update 两个状态,这意味着函数组件的每一次调用都会执行其内部的所有逻辑,那么会带来较大的性能损耗。因此 useMemo 和 useCallback 就是解决性能问题的杀手锏
对比
useMemo 和 useCallback , useEffect 最大的区别是 useEffect 会用于处理副作用,而前两个 hooks 不能
useMemo 和 useCallback 都会在组件第一次渲染的时候执行。
之后会在其依赖的变量发生改变时再次执行;
并且这两个 hooks 都返回缓存的值, useMemo 返回缓存的变量, useCallback 返回缓存的函数
useEffect
使用场景: useEffect 副作用,使函数组件拥有了类似 react 的声明周期。 useEffect 会在组件每次 render 之后调用, useEffect 有两个参数,第一个为执行函数,第二个为数组[]
如果你熟悉 React class 的生命周期函数,你可以把 useEffect Hook 看做 componentDidMount , componentDidUpdate 和 componentWillUnmount 这三个函数的组合
import React, { useState, useEffect } from 'react'; function Example() { const [count, setCount] = useState(0); const [dataSources, setDataSources] = useState([]); /* * 情况一:useEffect无第二个参数 */ //组件初始化和render后,都会执行该useEffect useEffect(() => { console.log("相当于生命周期:componentDidMount+componentDidUpdate") }); /* * 情况二:useEffect有第二个参数 */ //第二个参数为空数组时:组件初始化才执行 useEffect(() => { console.log("相当于生命周期:componentDidMount"); }, []); //第二个参数为指定状态值时:组件初始化时和dataSources发生变化才执行 useEffect(() => { console.log("相当于生命周期:componentDidMount") console.log("相当于依赖dataSources状态值的生命周期:componentDidUpdate") }, [dataSources]); //执行函数内return一个函数:初始化时执行函数体,组件卸载unmount时执行return后的函数 useEffect(() => { console.log("相当于生命周期:componentDidMount") // 执行函数中直接使用return返回一个函数,这个函数会在组件unmount时执行。 return () => { console.log('相当于声明周期:componentWillUnmount'); } }, []); return (); } export default Example;You clicked {count} times
官方提示:与 componentDidMount 或 componentDidUpdate 不同,** useEffect 是异步的,使用 useEffect 调度的 effect 不会阻塞浏览器更新屏幕,这让你的应用看起来响应更快。**官方建议尽可能使用 useEffect , effect 不需要同步地执行 在个别情况下(例如测量布局,页面状态值闪烁 bug 时),才使用 useLayoutEffect 代替 useEffect , 形成同步,在 DOM 更新完成后立即执行,但是会在浏览器进行任何绘制之前运行完成,阻塞了浏览器的绘制
useMemo
当[]依赖为空数组时候,可以初始化数据。
当[ count ]不为空时,每次 count 改变都会重新计算
const initGoods = useMemo( () => pageDetail.goods.map(item => { const { termList = [], isOpenTrainingCamp } = item; if (isOpenTrainingCamp && termList.length !== 0) { return { ...item, hasTerm: true, skuBizNo: termList[0].termId, termDate: termList[0].beginTime, }; } return item; }), [], ); const [goods, setGoods] = useState(initGoods);
useCallback
讲完了 useMemo ,接下来是 useCallback 。 useCallback 跟 useMemo 比较类似,但它返回的是缓存的函数。 const fnA = useCallback(fnB , [ a ] )
import React, { useState, useCallback } from 'react'; const set = new Set(); export default function Callback() { const [count, setCount] = useState(1); const [val, setVal] = useState(''); const callback = useCallback(() => { console.log(count); }, [count]); set.add(callback); return; }{count}
{set.size}
setVal(event.target.value)}/>
每次修改 count , set.size 就会+ 1 ,这说明 useCallback 依赖变量 count , count 变更时会返回新的函数;而 val 变更时, set.size 不会变,说明返回的是缓存的旧版本函数。
使用场景是:有一个父组件,其中包含子组件,子组件接收一个函数作为 props ;通常而言,如果父组件更新了,子组件也会执行更新;但是大多数场景下,更新是没有必要的,我们可以借助 useCallback 来返回函数,然后把这个函数作为 props 传递给子组件;这样,子组件就能避免不必要的更新。
import React, { useState, useCallback, useEffect } from 'react'; function Parent() { const [count, setCount] = useState(1); const [val, setVal] = useState(''); const callback = useCallback(() => { return count; }, [count]); return; } function Child({ callback }) { const [count, setCount] = useState(() => callback()); useEffect(() => { setCount(callback()); }, [callback]); return{count}
setVal(event.target.value)}/>{count}}