问题描述
出于封装img lazyload
高阶组件的需求导致的问题。由于需要获取到组件的img
对象,所以需要获取到传过来的组件的ref
,但是在高阶组件中需要通过ref
转发来实现,相对复杂难理解故而放弃。最终使用在组件外包装一层div
,获取此div
的dom
对象,来达到获取内部组件的img
对象。具体实现如下:
export default function withLazyLoad(WrappedComponent: any) {
// 懒加载图片
function lazyloadImg(node: Element | null, src: string) {
// ......
}
return (props: IWithLazyLoad) => {
const {
lazyload = true,
src,
loading = '',
error,
...compProps } = props
const baseComponentRef = React.createRef<HTMLDivElement>();
// 用于存储img对象
const imgRef = useRef<any>(null)
// 数据存在则触发懒加载
useEffect(() => {
if (lazyload && src && imgRef.current) {
lazyloadImg(imgRef.current, src)
}
}, [lazyload, src, imgRef.current])
// 获取img对象
useEffect(() => {
if (baseComponentRef.current) {
imgRef.current = baseComponentRef.current.querySelector('img')
}
}, [baseComponentRef.current])
return (
<div ref={baseComponentRef} className={wrapperClassName} title={placeholder}>
<WrappedComponent {...compProps} src={loading} />
</div>
)
}
}
问题表现
始终无法触发lazyloadImg
方法,导致懒加载功能失效。
滚动后可以表现正常。
本地开发时表现正常,发布到dev环境就出问题(本地开发会触发 组件卸载-挂载两个过程)
原因分析
通过debugger
可以发现,每次进行是否触发懒加载函数的条件判断时,imgRef.current
的值始终是null,导致未曾进入if分支。
解决办法
将两个useEffect交换顺序即可解决,很邪门是不是?那且听我给你慢慢分析。
三个问题思考👀
1. 为什么交换了顺序就能解决imgRef.current
取值始终为null
的问题?
为了解答这个问题,我们先来了解一下三个知识点:
- react的函数组件的加载机制
- uesEffect的触发机制
- useRef与createRef的区别
react的函数组件的加载机制
大家可以参考一下生命周期React生命周期详解
我简单阐述一下在函数组件中的加载过程(我的理解)
按照函数运行一样自上而下依次执行对应的语句,初始化state =》 初始化方法 =》按照出现顺序依次将 useEffect任务放到异步宏等待队列中 =》 渲染页面 =》 依次根据依赖执行useEffect方法定义的回调函数。如果回调中更改了state的值则重新进入这个加载流程,从而使视图更新。
辅助资料:
《React函数组件-useState原理》
React生命周期详解
useEffect的触发机制
常规上认为useEffect
的依赖发生变化时,useEffect
里的回调就会被调用。但是实际是因为,常规情况下useEffect
的第二个参数中,传递的都是能够引起视图更新的变量。变量更改,视图更新,useEffect
进行重新加载,对应的任务重新放置到异步执行队列中,从而产生 依赖的变量更新后,uesEffect
回调被调用的效果。
useEffect
的清除函数在每次重新渲染时都会执行,而不是只在卸载组件的时候执行
useRef与createRef的区别
useRef
只能在hook
里使用,在后续组件重新渲染中保持引用值不变。用于获取元素的dom
值时,是在组件渲染完成时才赋值,因此useEffect
可能检测不到它的变更。想要实现ref
转发的话需要结合forwardRef
来实现。createRef
常用于类组件中,也可在hook
组件中使用。在组件重新渲染时,引用值在不停的发生变化。
2. 为什么滚动页面就能触发?
页面滚动时,组件进行了重新渲染,在上一次渲染时已经对ref
的current
赋值。再次触发useEffect
时,就可以正常调用懒加载方法,从而生效。
3.为什么经历组件卸载-挂载两个过程就能正常表现?卸载过程做了什么?
在两个副作用函数中进行打印,打印结果告知
// 数据存在则触发懒加载
useEffect(() => {
console.log('mount', imgRef.current)
if (lazyload && src && imgRef.current) {
lazyloadImg(imgRef.current, {})
}
return () => {
console.log('unmount img', imgRef.current)
}
}, [lazyload, src, imgRef.current])
// 获取img对象
useEffect(() => {
console.log('mount', baseComponentRef.current)
if (baseComponentRef.current) {
imgRef.current = baseComponentRef.current.querySelector('img')
}
return () => {
console.log('unmount base', imgRef.current)
}
}, [baseComponentRef.current])
本地开发时,组件会经历挂载-卸载-挂载的过程,从而产生在第一次挂载时给imgRef
设置了current
的值,卸载时imgRef
仍然保存着,所有在下一次正式挂载时,触发懒加载的依赖回调就会被执行,从而避免了线上未能触发懒加载的现象。