文章转自https://caibowen.net/article/5a5ad0692f7d
常用的官方 hooks(官方文档)
useState
这个应该是我们最常用的 hook 了,用来代替 class 组件中的 setState 基础用法:const [state, setState] = useState(initialState)
;
**需要注意的点【查看代码】 **
-
setState 这里接受的参数除了一个给定的值外,还可以接受一个函数。例如
const [count, setCont] = useState(0); setCount(count+1) setCount((prevCount) => prevCount + 1) // 上面用法通常使用在,我没法保证我获取的 count 是最新的值的情况下,比如在 useCallback 中 const fn = useCallback(() => { console.log(count); // 这里的count 会一直是初始化的那个count,这个时候如果用 setCount(count + 1) 就会出现问题 setCount((prevCount) => prevCount + 1); // 这样就不会有问题 }, []) // 这里最好传递要用到的依赖,否则会被缓存,获取不到最新的值
-
setState 会对值进行比较,如果 prevValue 跟这次的 value 相等的话(比较只是进行了浅比较),将不会触发更新。如果想强制每次更新,可以自定义一个hooks,例如
// 如果是class 组件,每次调用setState 都会触发render,它不会对前后的值进行比较 const useFocusUpdate = () => { const [ignore, update] = useState(0); return () => { update(ignore + 1); }; };
initialState 参数只会在组件的初始渲染中起作用,后续渲染时会被忽略。
useEffect
基础用法: useEffect(fn, [deps])
【后面的Fn, deps,均指的此处,Fn2指的是 Fn的返回函数】 默认情况下useEffect 接受的函数 Fn 会在 render 之后执行,Fn 执行后,如果返回了一个函数Fn2,那么Fn2,在执行下一个 effect 之前,即上一个 effect 销毁时
export default function App() {
console.log('start')
const [count, setCount] = useState(0);
useEffect(() => {
console.log('log on effect', count);
return () => {
console.log('log on effecct return fn', count);
}
}, [count])
console.log('render')
return (
<div className="App">
<h1>{count}</h1>
<h2 onClick={() => setCount(count + 1)}>Add Count</h2>
</div>
);
}
// 打印顺序
// start render log on effect 0
// 点击 Add Count 触发 count 更新,打印顺序
// start render log on effecct return fn 0 log on effect 1
需要注意的点
在Fn中使用到的变量需要传入的 deps,否则会出现取不到最新的值的问题
-
传入 deps 的值也需要注意,不要传递函数中的方法,例如
// 这样会造成死循环,因为每一次重新 render handle 就会被重新创建,useEffect 每次都会检测到变化,每次就会执行 function App() { const [count, setCount] = useState(0); const handle = () => { setCount(count + 1) }; useEffect(() => { handle(); }, [fn]) return ( <div className="App"> </div> ); }
useEffect 经常被用到的使用场景
模拟 DidMount useEffect(fn, []) 传入一个空数组即可,fn 会在首次 render 后执行,之后再也不会执行,fn 返回的函数会在组件销毁时执行,即为 willUnmount
-
用来监变量变化,例如
// 当 city 出现变化后,我们重新开始拉取 城市信息,城市下面的运营信息,城市下面的POI等信息 useEffect(() => { if (cityId !== null) { fetchCityInfo(); fetchCategoryList(); fetchOperationResource(); fetchPoiList(); } }, [cityId]);
useCallback
把内联回调函数及依赖项数组作为参数传入 useCallback,它将返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新。
为什么会有这个hook呢?因为前面说的,组件更新的时候,组件内的内联函数都会被重新创建,如果我们将这个内联函数变成依赖,或者传递给某个子组件,都会导致一些性能问题。
应用场景
// 这种写法,如果 App 需要re-render,Children 也会出现 re-render,因为handle这个函数每次都被重新被创建了
function App() {
const handle = () => { ... }
return (
<div className="App">
<Children onClick={handle}/>
</div>
);
}
function App() {
const handle = useCallback(fn, [deps]); // fn 中如果用到了某个state,需要放在deps中,否则拿到的 state 不是最新的
return (
<div className="App">
<Children onClick={handle}/>
</div>
);
}
useMemo
把“创建”函数和依赖项数组作为参数传入 useMemo,它仅会在某个依赖项改变时才重新计算 memoized 值。这种优化有助于避免在每次渲染时都进行高开销的计算。
useCallback(fn, deps) 相当于 useMemo(() => fn, deps)
应用场景
-
需要复杂计算的时候,可以用来存储, 例如
function App() { // computeExpensiveValue 是一个非常复杂的计算函数,这样做了后,只要依赖的 a 不发生变化, // computeExpensiveValue是不会重复执行的,value是一个 memoized 值。 const value = useMemo(() => computeExpensiveValue(a), [a]); return ( <div className="App"> {value} </div> ); }
-
用 useMemo 来优化引用数据的传递,例如
// 这种写法,如果 App 需要re-render,Children 也会出现 re-render,因为 Children 的 style 这个值一直都被重新创建 function App({ height }) { return ( <div className="App"> <Children style={{ height }}/> </div> ); } // 用 useMemo 来记忆,减少 Children 的 re-render function App({ height }) { const childrenStyle = useMemo(() => ({ height }), [height]) return ( <div className="App"> <Children style={childrenStyle}/> </div> ); }
useRef
useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内保持不变。
基本用法跟注意点【查看代码】
-
用来访问dom节点,或者访问一堆列表的节点
// 注意 需要首次渲染结束后,ref.current 上面才有对应的 dom。如果 dom 被移除了对应的 ref.currnt 就会变成 null export default function App() { const [show, setVisible] = useState(true); const ref = useRef(null); const listRef = useRef([]); useEffect(() => { console.log(listRef); }, [ref2]) return ( <div className="App"> {show && <div ref={ref}>dom1</div>} {ary.map((num, index) => <div key={num} ref={(_ref) => { listRef.current[index] = _ref; }}>{num}</div>)} <div onClick={() => { console.log(ref.current); setVisible(!show) }}>click me</div> </div> ); }
-
ref是不能被监听的
export default function App() { const ref2 = useRef(0); useEffect(() => { // ref2 改变后 并不会重新触发此函数 console.log(ref2); }, [ref2]) return ( <div className="App"> <div onClick={() => { ref2.current = Math.random(); }}> change ref</div> </div> ); }
-
用来存储一些不用引起 re-render 但是需要变化的变量。例如
export default function App() { const hadSendMc = useRef(false); return ( <div className="App"> <div onClick={() => { if(hadSendMc.current){ return; } console.log('send mc') hadSendMc.current = true; }}>send mc</div> </div>
useImperativeHandle
useImperativeHandle 可以让你在使用 ref 时自定义暴露给父组件的实例值。在大多数情况下,应当避免使用 ref 这样的命令式代码。useImperativeHandle 应当与 forwardRef 一起使用。 我在项目中只用到了一次,主要是给父组件暴露自定义的方法,而不是一股脑的直接返回ref,房子父组件操作ref导致问题
使用示例
const Video = ({ ... }: PropTypes, ref: any) => {
const videoRef: any = useRef(null);
const playVideo = () => {...};
const handlePlayBtnClick = () => {...};
useImperativeHandle(ref, () => ({
play: playVideo,
pause: () => {
if (videoRef.current) {
videoRef.current.pause();
setVideoVisible(false);
}
},
}));
return (
<div className={styles.container}>
<video ref={videoRef}>
<source src={videoUrl} type="video/mp4" />
</video>
</div>
);
};
export default React.memo(React.forwardRef(Video));
后续如果使用了,我会同步补上 useContext (这个我没有用过,略) useReducer (这个我没有用过,略) useLayoutEffect (这个我没有用过,略) useDebugValue (这个我没有用过,略)
常用的优化手段
使用 React.memo 去优化 由于 Function Component 组件没有类似 shouldComponentUpdate 这样的方法,也没法继承 PureComponent,我们就可以使用 React.memo去做优化 注意: React.memo仅影响 props 变更。如果函数组件被 React.memo 包裹,且其实现中拥有 useState 或 useContext 的 Hook,当 context 发生变化时,它仍会重新渲染。 使用示例:
React.memo(MyComponent, areEqual);
默认情况下其只会对复杂对象做浅层对比,如果你想要控制对比过程,那么请将自定义的比较函数通过第二个参数传入来实现。利用 useCallback 跟 useMemo 去缓存数据,具体可以看上面的示例
如果使用了 react-redux,可以通过 useSelect 的第二个参数去做优化 使用示例:const result : any = useSelector(selector : Function, areEqual? : Function)
项目中自定义的 hook 梳理
针对具体的业务具体开发,笔者这里只是抛砖迎玉
-
发送页面PV
export const usePageView = (pageId) => { const hadSendPv = useRef(false); useEffect(() => { if (hadSendPv.current) { return; } sendPv(); }, [pageId]); };
-
useIntersectionObserver 用来监听 dom 是否出现在可视窗口。注意需要引用 intersection-observer-polyfill
export const useIntersectionObserver = (cb: ObserveCallback, ref?: any, startObserver = true) => { const io: any = useMemo(() => new IntersectionObserver((IntersectionObserverEntryList: any) => cb(IntersectionObserverEntryList, io)), [cb]); const observerRef: any = useRef(ref || null); useEffect(() => { if (!observerRef.current || !startObserver) { return; } if (observerRef.current instanceof Array) { const eleList = observerRef.current; eleList.forEach((ele: any) => { io.observe(ele); }); return () => { eleList.forEach((ele: any) => { io.unobserve(ele); }); }; } const ele = observerRef.current; io.observe(ele); return () => { io.unobserve(ele); }; }, [io, startObserver]); return observerRef; }; // 使用方法 function App({ height }) { const observerHandle = useCallback(() => { ... }, []); const observerRef = useIntersectionObserver(observerHandle) return ( <div className="App" ref={observerRef}> <Children style={{ height }}/> </div> ); }
-
抽离微信分享设置
const environment = { ... }; export const useWechatShare = (shareInfo: any) => { if ((environment.isMini() || environment.isWx) && shareInfo.shareUrl) { // 微信分享设置 } }, [shareInfo, environment]); };
-
useScroll 页面滚动函数绑定 注意:此处不是相对于body滚动的!
export const useScroll = (handle: ScrollHandle, threshHold: number = 250) => { const handleWrapper = useCallback(throttle(handle, threshHold), [handle]); const containerId = 'containerId'; useEffect(() => { let listerEle: any = window; if (containerId) { listerEle = document.getElementById(containerId); } console.log('useScroll 注入成功'); if (listerEle) { listerEle.addEventListener('scroll', handleWrapper); return () => { listerEle.removeEventListener('scroll', handleWrapper); }; } }, [handleWrapper, containerId]); }
总结
- react hooks 的出现提供了我们更好的模块化代码的方式,我们从以前的抽离公共UI的方式,可以变为更小粒度的拆分,例如拆分某个公共的逻辑
- react hooks 能够大大的减少我们大代码量,不需要再去理解复杂的class Component,一切都变的更加简单了
- 在使用的过程中一定要注意到,Function Component 始终是函数,只要re-render,里面的代码都会重新执行,内联函数,内联对象都会被重新创建
最后再立个 flag
用 react hooks 模拟 class 的各个生命周期