useCallback
功能和缺点:生成一个根据依赖项变化的方法。可以防止函数每次生成新的地址。传给其他组件后影响性能。
缺点:1、每次更新依赖项的时候,返回的方法会更新,导致子组件重新渲染。2、不在依赖项里的数据就无法获取最新的。
解决方案(可以同时解决上面2个缺点):
1、useEvent :react官方出的一个,可以返回一个地址不变的方法,且没有依赖项的,可以提高性能,且数据永远最新的。(但是被废除了)
2、useMemorizedFun:功能同上,是ahooks的方法。
useMemo
useLayoutEffect 和 useEffect
useLayoutEffect文档:参考1 的例子 + 参考2 的结论 参考1参考2
总结:
1、顺序问题。先useLayoutEffect,后useEffect
2、快照问题:useLayoutEffect使用了快照,用的是closure
3、执行时机:useLayoutEffect 和componentDidUpdate 是一样的,state更新后 ,页面渲染完成前调用。因此useLayoutEffect里setState并影响页面渲染时,不会出现闪动,但是可以获取到真实dom。具体请查看:点击
4、阻塞问题:useLayoutEffect里setState。会阻断渲染重新再走一次 ,虚拟dom-diff-渲染前-渲染-渲染后。
5、useLayoutEffect会影响服务端渲染的首屏加载
服务端渲染的解释(next.js):click
useEvent
useEvent参考文档:参考1 参考2
下图是 useEvent的大致的源码。精髓就是:handler放到ref.current里保存。返回的是ref.current的引用。当任意state发生变化时,即可冲洗执行ref.current = handler 此时的闭包环境就变了。
handler所在的闭包closure就变了。就能访问到所有变量的最新值了。
useMemorizedFun
注意: useMemorizedFun与useCallback 的使用场景。useLayoutEffect 和 useEffect的使用场景。
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable react/button-has-type */
import React, {
useCallback,
useEffect,
useLayoutEffect,
useMemo,
useRef,
useState,
} from 'react';
function useEvent(handler: any) {
const handlerRef = useRef(null);
// In a real implementation, this would run before layout effects
// 这行保证handlerRef一直处于,最新的闭包、拿到最新的数据
/*
首先,由于每次父组件更新,都会执行到 下面的代码。这个是首要条件!
const changeFun_useEvent = useEvent(() => {
console.log('useEvent closure 里的 State', State);
});
其次,每次执行都会传递过来一个新的handler,自然带来了新的闭包、新的数据。
最后,useLayoutEffect的作用是每次数据发生更新,自动的去执行里面的方法。
所以 handlerRef.current 就能拿到带着最新数据的一个handler
*/
useLayoutEffect(() => {
console.log('useLayoutEffect执行了');
handlerRef.current = handler;
});
/*
首先, useCallback 并且依赖为[],只执行一次,地址也就永远不会变了。
那么,useEvent()return出去函数的地址永不变化。
可以解释为
1、useEvent() return 出去了一个对象,为 obj = {A:{}}
2、 useCallback里的箭头函数return出去的是这个 fn=handlerRef.current;相当于一个属性 A
由于handlerRef.current指向地址是不停变化的,所以A得指向地址就是不停变化的。
3、因此我们真正使用 changeFun_useEvent=useEvent(xx)的时候, 拿到的是obj。永远不变的地址。
changeFun_useEvent()调用的时候拿到的就是obj.A了。就是最新的带着最新数据的一个handler
*/
return useCallback((...args: any) => {
// In a real implementation, this would throw if called during render
const fn = handlerRef.current;
return fn(...args);
}, []);
}
function useMemorizedFn(fn: any) {
const fnRef = useRef(fn);
// 这行保证fn一直处于,最新的闭包、拿到最新的数据。
/**
* fn在这里做依赖的原来: 由于每次父组件重新执行,都会走到useMemorizedFn里并传过来一个新的箭头函数。所以fn每次地址都是新的。也就带来了新数据的闭包
* */
fnRef.current = useMemo(() => {
console.log('useMemo执行了'); // 父级改变就执行
return fn;
}, [fn]);
const memoizedFn = useRef<any>();
if (!memoizedFn.current) {
console.log('memoizedFn');
// 这行保证fn引用地址不变、
// 这里可以解释为: const obj = {A:{}} ; memoizedFn.current = obj;
//当执行memoizedFn.current的时候也就是去访问了 obj.A
memoizedFn.current = function (this: any, ...args: any) {
return fnRef.current.apply(this, args);
};
}
return memoizedFn.current;
}
const TestChild = (props: any) => {
const { changeFun } = props;
useEffect(() => {
console.log('changeFun-》地址change了,导致了子组件刷新了。');
}, [changeFun]);
return (
<div style={{ backgroundColor: 'pink' }}>
子组件
<br />
<br />
<button onClick={changeFun}>child click - get parent's state </button>
</div>
);
};
React.memo(TestChild);
const Test: React.FC<any> = () => {
const [State, setState] = useState(0);
// const changeFun = useEvent(() => {
// console.log('useEvent closure 里的 State', State);
// });
const changeFun1 = useCallback(() => {
console.log('useCallback closure 里的 State', State);
}, [State]);
const changeFun_usememrized = useMemorizedFn(() => {
console.log('useMemorizedFn closure 里的 State', State);
});
// // 加载顺序问题 和 快照问题
// useEffect(() => {
// console.log('加载了');
// }, []);
// useLayoutEffect(() => {
// console.log('State', State);
// console.log('加载了-useLayoutEffect');
// setTimeout(() => {
// console.log('State', State);
// }, 3000);
// }, [State]);
return (
<div style={{ width: '100%', padding: '20px', backgroundColor: 'green' }}>
父组件的state: {State}
<br />
<br />
<br />
<button onClick={() => setState(Math.random())}>
click-change state
</button>
<br />
<br />
<br />
<TestChild changeFun={changeFun_usememrized}></TestChild>
</div>
);
};
export default Test;