本人刚刚入职新公司,以前都是写Vue的,现在新公司技术栈使用的是react。一顿恶补后在实际的项目中还是避免不了踩坑,花大量的时间找原因,debug。。。不知所措的想哭QAQ
公司项目中需要实现一个滚动分页加载数据得效果,按照咱们逻辑应该是这样的:
1.请求前先判断loading是否为true, 为true时return掉阻止请求函数调用,为false时将loading设置为true然后发起请求
2.请求完毕后再将loading设置为false,允许下次再发起请求
import React, { useEffect, useRef, useState } from 'react'
import styles from './index.module.scss'
import { LoadingOutlined } from '@ant-design/icons';
import {getScrollLoadList} from '../../api/scrollLoad'
function ScrollLoad() {
const [list, setList] = useState<number[]>([])
const [pageNum, setPageNum]=useState<number>(1)
const [loading, setLoading] = useState<boolean>(false)
const wrapRef = useRef<any>(null)
useEffect(() => {
const Dom = wrapRef.current
Dom.addEventListener('scroll',loadMore)
return () => {
Dom.removeEventListener('scroll',loadMore)
}
// eslint-disable-next-line
},[])
useEffect(() => {
getList()
// eslint-disable-next-line
},[pageNum])
const getList = () => {
setLoading(true) // 设为请求状态
getScrollLoadList({pageNum}).then((res:any) => {
const temp = res.result
const nowList = pageNum === 1 ? temp : [...list,...temp]
setList(nowList)
}).finally(() => {
setLoading(false) // 请求完毕置为false
})
}
const loadMore = (e:any) => {
const {offsetHeight, scrollTop, scrollHeight} = e.target
if(offsetHeight + scrollTop === scrollHeight) {
if(loading) return // 判断是否在请求状态
setPageNum((pageNum)=> pageNum + 1)
}
}
return (
<div ref={wrapRef} className={styles.scroll_wrap}>
{
list && list.length && list.map(item => (
<div key={item} className={styles.wrap_item}>{item}</div>
))
}
{loading && <div className={styles.loading}><LoadingOutlined /></div> }
</div>
)
}
export default ScrollLoad
咋一看好像代码没啥问题啊,但实际上问题大的去了。当连续快速的滚动时,这货始终能调用接口。loadingMore函数里的if(loading) return 并没有产生什么卵用,并且打印出来始终为我们的初始值false,百思不得其解!!!
那么为什么出现这种问题呢?经过一番研究。是因为useEffect(() => {},[])这个副作用相当于class组件内的生命周期componentDidMount,在组件渲染中只执行一次。
重点来了
当上面代码useEffect(() => {},[])执行时会产生闭包,里面用到的state变量会进行缓存,只要这个闭包不被释放,里面的state变量就不会是最新的值。即loading始终为初始状态下的值false。
那么怎么解决这个问题呢?
方案1:
可以将pageNum这个参数传进去即useEffect(() => {},[loading]),当loading改变后,会销毁之前的闭包,产生新的闭包,这样就能保证里面使用的state变量是最新的,不过这种方法每次都得重新获取dom元素,设置监听和移除监听事件,比较耗性能。(useEffect(() => {})也可以,但是不传只要有状态变化就会销毁和新建相对来说更耗性能)
改动代码如下:
useEffect(() => {
const Dom = wrapRef.current
Dom.addEventListener('scroll',loadMore)
return () => {
Dom.removeEventListener('scroll',loadMore)
}
// eslint-disable-next-line
},[loading]) // 传入loading,监听loading变化
方案2
通过设置一个局部变量。 可以在函数组件外定义一个变量或者函数内使用useRef()创建一个变量(这里简称loadingRef),然后将state值loading赋值给这个变量,当loading改变时,会触发loadingRef的改变,这样就会保证loadingRef是最新的值,然后通过loadingRef活loadingRef.current 去判断即可
import React, { useEffect, useRef, useState } from 'react'
import styles from './index.module.scss'
import { LoadingOutlined } from '@ant-design/icons';
import {getScrollLoadList} from '../../api/scrollLoad'
// let loadingRef = false
function ScrollLoad() {
const [list, setList] = useState<number[]>([])
const [pageNum, setPageNum]=useState<number>(1)
const [loading, setLoading] = useState<boolean>(false)
const wrapRef = useRef<any>(null)
const loadingRef = useRef<boolean>()
loadingRef.current = loading
// loadingRef = loading
useEffect(() => {
const Dom = wrapRef.current
Dom.addEventListener('scroll',loadMore)
console.log(Dom,loading)
return () => {
console.log('清空监听事件')
Dom.removeEventListener('scroll',loadMore)
}
// eslint-disable-next-line
},[])
useEffect(() => {
getList()
// eslint-disable-next-line
},[pageNum])
const getList = () => {
setLoading(true)
getScrollLoadList({pageNum}).then((res:any) => {
const temp = res.result
const nowList = pageNum === 1 ? temp : [...list,...temp]
setList(nowList)
}).finally(() => {
setLoading(false)
})
}
const loadMore = (e:any) => {
const {offsetHeight, scrollTop, scrollHeight} = e.target
if(offsetHeight + scrollTop === scrollHeight) {
console.log(loadingRef, '下拉加载之前')
// if(loadingRef) return
if(loadingRef.current) return
setPageNum((pageNum)=> pageNum + 1)
}
}
return (
<div ref={wrapRef} className={styles.scroll_wrap}>
{
list && list.length && list.map(item => (
<div key={item} className={styles.wrap_item}>{item}</div>
))
}
{loading && <div className={styles.loading}><LoadingOutlined /></div> }
</div>
)
}
export default ScrollLoad