React?懸浮框內(nèi)容懶加載實(shí)例詳解
界面隱藏
一個(gè)容器放置視頻,默認(rèn)情況下
display: none; z-index: 0; transform: transform3d(10000px, true_y, true_z);
y軸和z軸左邊都是真實(shí)的(騰訊視頻使用絕對(duì)定位,因此是計(jì)算得到的),只是將其移到右邊很遠(yuǎn)的距離。
懶加載
React監(jiān)聽(tīng)鼠標(biāo)移入(獲取坐標(biāo))
- 添加事件監(jiān)聽(tīng)
onMouseEnter={(e) => { handleMouseEnter(e) }}
const handleMouseEnter = (e: React.MouseEvent) => {
console.log(e.target)
}
注意事件類(lèi)型是React.MouseEvent。

typescript中HTMLElement 和 Element的區(qū)別
ts中:
let res =document.getElementById('test'); //HTMLElement
let el = document.querySelector('#test'); // Element
mdn中: querySelector,getElementById兩者均返回Element。
Element 是一個(gè)通用性非常強(qiáng)的基類(lèi),所有 Document 對(duì)象下的對(duì)象都繼承自它。這個(gè)接口描述了所有相同種類(lèi)的元素所普遍具有的方法和屬性。一些接口繼承自 Element 并且增加了一些額外功能的接口描述了具體的行為。
例如, HTMLElement 接口是所有 HTML 元素的基本接口,而 SVGElement 接口是所有 SVG 元素的基礎(chǔ)。大多數(shù)功能是在這個(gè)類(lèi)的更深層級(jí)(hierarchy)的接口中被進(jìn)一步制定的。
實(shí)現(xiàn):
function getElementAbsPos(e: HTMLElement) {
var t = e!.offsetTop;
var l = e!.offsetLeft;
while (e = e!.offsetParent as HTMLElement) {
t += e.offsetTop;
l += e.offsetLeft;
}
return { left: l, top: t };
}
React實(shí)現(xiàn)
在騰訊視頻中,懸浮框是處于頂層div下的,因此使用絕對(duì)定位(絕對(duì)定位是相當(dāng)與父節(jié)點(diǎn)的,并不是document)。
在React中,由于我們將展示視頻信息的這個(gè)Item組件化了,因此實(shí)現(xiàn)思路有一點(diǎn)改變:
- 每個(gè)Item都有一個(gè)對(duì)應(yīng)的懸浮框DIV,默認(rèn)情況
hidden; - 為了節(jié)省流量,懸浮框內(nèi)的內(nèi)容需要懶加載;
- 顯示懸浮框的時(shí)機(jī)是一致的——鼠標(biāo)移入時(shí),為了優(yōu)化體驗(yàn),節(jié)省流量,可以設(shè)定為移入一段時(shí)機(jī)后才顯示;
原始代碼
import { Card } from 'antd';
import { Content } from 'antd/lib/layout/layout';
import { useState } from 'react';
import { useNavigate } from 'react-router-dom'
import styles from './css/VideoItem.module.css'
interface VideoItemProps {
video: Video,
topCategory?: string,
subCategory?: string,
}
export default function VideoItem(props: VideoItemProps) {
const { video, topCategory, subCategory } = props
const navigate = useNavigate()
const [loading, setLoding] = useState(false)
const to = (() => {
let itemTop = Object.getOwnPropertyNames(video.category)[0]
let itemSub = video.category[itemTop].length ? video.category[itemTop][0] : ''
if (topCategory) {
itemTop = topCategory
itemSub = ''
if (subCategory) {
itemSub = subCategory
}
}
if (itemSub) {
return `/detail/${itemTop}/${itemSub}/${video.id}`
} else {
return `/detail/${itemTop}/${video.id}`
}
})()
return (
<NavLink to={to}>
<Card hoverable
bordered={false}
style={{ width: 180, height: 280, overflow: 'hidden' }}
bodyStyle={{ padding: 4 }}
className={styles.video}
cover={<img style={{ width: '180px', height: '230px' }} alt={video.title} src={video.poster} />}
onMouseOver={() => { }}
onClick={() => handleClick()}
>
<div style={{ height: 280 }}>
{video.title}
</div>
</Card>
</NavLink>
)
}
handleClick響應(yīng)點(diǎn)擊事件,跳轉(zhuǎn)到視頻詳情頁(yè),以上代碼還不含與本文相關(guān)內(nèi)容。
放入新的DIV
<NavLink to={to}>
<Card
bordered={false}
bodyStyle={{ padding: 4 }}
className={styles.video}
cover={<img alt={video.title} src={video.poster} />}
>
<div className={styles.title}>
{video.title}
</div>
</Card>
<Card hoverable
bordered={false}
style={{
backgroundColor: 'pink',
display: hiddenDetail ? 'none' : 'inline-block',
position: 'absolute',
transform: `translate3d(0px, -100%, 0px)`,
}}
bodyStyle={{ padding: 4 }}
className={styles.video}
cover={<img alt={video.title} src={'占位圖鏈接'} />}
>
<div className={styles.title}>
{video.title}
</div>
</Card>
</NavLink>
狀態(tài)設(shè)置
加入狀態(tài)表示是否隱藏懸浮框:
默認(rèn)隱藏
const [hiddenDetail, setHiddenDetail] = useState(true)
樣式設(shè)置
style={{
backgroundColor: 'pink',
display: hiddenDetail ? 'none' : 'inline-block',
position: 'absolute',
transform: `translate3d(0px, -100%, 0px)`,
}}
兩個(gè)Card組件的寬度和高度已經(jīng)設(shè)為一致,為了方便調(diào)試,將懸浮框的背景設(shè)為粉色;
使用絕對(duì)定位,讓其能夠覆蓋原始信息;
通過(guò)transform改變懸浮框的位置,不設(shè)置的話(huà),懸浮框默認(rèn)被擠到下方,-100%表示在y軸上向上移動(dòng)懸浮框高度對(duì)應(yīng)的像素,由于兩個(gè)Card組件高度相同,因此可以覆蓋原始信息。
事件設(shè)置
第一個(gè)Card,即默認(rèn)顯示的元素,添加鼠標(biāo)移入事件:
onMouseEnter={(e) => {
setHiddenDetail(!hiddenDetail)
}}
第二個(gè)Card,即懸浮框,添加鼠標(biāo)移出事件:
onMouseLeave={(e) => {
// bug 向下移出不會(huì)觸發(fā)
// 因?yàn)橐迫肓说讓覥ard,執(zhí)行了setHiddenDetail(false)
// 將移入事件改為 setHiddenDetail(!hiddenDetail)
setHiddenDetail(true)
}}
這里我們使用!hiddenDetail,而不是直接設(shè)為true,
因?yàn)槿绻讓?code>DIV大于懸浮框的框的話(huà),在懸浮框顯示的情況下,如果移出過(guò)程進(jìn)入了底層DIV,會(huì)導(dǎo)致懸浮框不會(huì)消失(雖然移出過(guò)程觸發(fā)了onMouseLeave,將狀態(tài)設(shè)為false,但移入底層DIV后,再次觸發(fā)onMouseEnter,將狀態(tài)設(shè)為true),這主要是應(yīng)對(duì)懸浮框沒(méi)有完全覆蓋底層元素的情況。

事件優(yōu)化
延遲顯示懸浮框
在底層元素的事件響應(yīng)中:
onMouseEnter={(e) => { setHiddenDetail(!hiddenDetail)}}
將狀態(tài)改變?nèi)蝿?wù)用Timeout包裹,設(shè)定延時(shí)t,如果在移出該元素時(shí),定時(shí)器還沒(méi)有結(jié)束,則結(jié)束該定時(shí)器:
let loadDetailJob: NodeJS.Timeout | null = null
<Card
bordered={false}
bodyStyle={{ padding: 4 }}
className={styles.video}
cover={<img alt={video.title} src={video.poster} />}
onMouseEnter={(e) => {
loadDetailJob = setTimeout(() => {
setHiddenDetail(!hiddenDetail)
}, 500)
}}
onMouseLeave={(e) => {
if (loadDetailJob) {
clearTimeout(loadDetailJob)
}
}}
>
<div className={styles.title}>
{video.title}
</div>
</Card>
懸浮框內(nèi)容懶加載
在騰訊視頻中,懸浮框顯示一小段視頻,但是一個(gè)頁(yè)面中包含多個(gè)懸浮框,如果一次全部加載這些資源,會(huì)造成比較大的流量浪費(fèi),因此,最后是要顯示懸浮框時(shí),才加載詳細(xì)內(nèi)容。
在本示例中,我們懸浮框顯示的圖片設(shè)為懶加載模式,我們需要增加一個(gè)狀態(tài)firstLoad記錄是否是第一次顯示懸浮框,如果是第一次,則設(shè)一個(gè)定時(shí)器模擬發(fā)送請(qǐng)求,獲取詳細(xì)內(nèi)容的鏈接。另一種情況是,在知道鏈接地址的情況下,不發(fā)送請(qǐng)求,將元素的src指向更高為正確的就行。
為了方便操作DOM元素,我們創(chuàng)建一個(gè)懸浮框的ref對(duì)象:detailRef。
const [firstLoad, setFirstLoad] = useState(true)
const detailRef = useRef<HTMLDivElement | null>(null)
useEffect(() => {
// 第一次加載懸浮框,并且懸浮框狀態(tài)為顯示
if (firstLoad && !hiddenDetail) {
// 在知道路徑的情況下,可以直接修改路徑,Promise用于模擬向服務(wù)器發(fā)送請(qǐng)求的等待過(guò)程
new Promise((resolve, reject) => {
setTimeout(() => {
resolve('load success')
}, 1000)
}).then(() => {
setFirstLoad(false)
detailRef.current!.querySelector('img')!.src = 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png'
})
}
}, [hiddenDetail])
完整代碼
import { Card } from 'antd';
import { Content } from 'antd/lib/layout/layout';
import { useEffect, useRef, useState } from 'react';
import { Image } from 'antd'
import { NavLink, useNavigate } from 'react-router-dom'
import styles from './css/VideoItem.module.css'
interface VideoItemProps {
video: Video,
topCategory?: string,
subCategory?: string,
}
function getElementAbsPos(e: HTMLElement) {
var t = e!.offsetTop;
var l = e!.offsetLeft;
while (e = e!.offsetParent as HTMLElement) {
t += e.offsetTop;
l += e.offsetLeft;
}
return { left: l, top: t };
}
export default function VideoItem(props: VideoItemProps) {
const { video, topCategory, subCategory } = props
const navigate = useNavigate()
const [hiddenDetail, setHiddenDetail] = useState(true)
const [firstLoad, setFirstLoad] = useState(true)
const detailRef = useRef<HTMLDivElement | null>(null)
let loadDetailJob: NodeJS.Timeout | null = null
const to = (() => {
let itemTop = Object.getOwnPropertyNames(video.category)[0]
let itemSub = video.category[itemTop].length ? video.category[itemTop][0] : ''
if (topCategory) {
itemTop = topCategory
itemSub = ''
if (subCategory) {
itemSub = subCategory
}
}
if (itemSub) {
return `/detail/${itemTop}/${itemSub}/${video.id}`
} else {
return `/detail/${itemTop}/${video.id}`
}
})()
useEffect(() => {
if (firstLoad && !hiddenDetail) {
new Promise((resolve, reject) => {
setTimeout(() => {
resolve('load success')
}, 1000)
}).then(() => {
setFirstLoad(false)
detailRef.current!.querySelector('img')!.src = 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png'
})
}
}, [hiddenDetail])
return (
<NavLink to={to}>
<Card
bordered={false}
bodyStyle={{ padding: 4 }}
className={styles.video}
cover={<img alt={video.title} src={video.poster} />}
onMouseEnter={(e) => {
loadDetailJob = setTimeout(() => {
setHiddenDetail(!hiddenDetail)
}, 500)
}}
onMouseLeave={(e) => {
if (loadDetailJob) {
clearTimeout(loadDetailJob)
}
}}
>
<div className={styles.title}>
{video.title}
</div>
</Card>
<Card hoverable
bordered={false}
loading={firstLoad}
ref={(c) => { detailRef.current = c }}
style={{
backgroundColor: 'pink',
display: hiddenDetail ? 'none' : 'inline-block',
position: 'absolute',
transform: `translate3d(0px, -100%, 0px)`,
}}
bodyStyle={{ padding: 4 }}
className={styles.video}
cover={<img alt={video.title} src={'占位圖片鏈接'} />}
onMouseLeave={(e) => {
// bug 向下移出不會(huì)觸發(fā)
// 因?yàn)橐迫肓说讓覥ard,執(zhí)行了setHiddenDetail(false)
// 將移入事件改為 setHiddenDetail(!hiddenDetail)
setHiddenDetail(true)
}}
>
<div className={styles.title}>
{video.title}
</div>
</Card>
</NavLink>
)
}以上就是React 懸浮框內(nèi)容懶加載實(shí)例詳解的詳細(xì)內(nèi)容,更多關(guān)于React 懸浮框內(nèi)容懶加載的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
react中setState的執(zhí)行機(jī)制詳解
setState() 的執(zhí)行機(jī)制包括狀態(tài)合并、批量更新、異步更新、虛擬 DOM 比較和渲染組件等步驟,這樣可以提高性能并優(yōu)化渲染過(guò)程,這篇文章主要介紹了react中的setState的執(zhí)行機(jī)制,需要的朋友可以參考下2023-10-10
解決React報(bào)錯(cuò)Cannot?find?namespace?context
這篇文章主要為大家介紹了React報(bào)錯(cuò)Cannot?find?namespace?context分析解決,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-12-12
使用useImperativeHandle時(shí)父組件第一次沒(méi)拿到子組件的問(wèn)題
這篇文章主要介紹了使用useImperativeHandle時(shí)父組件第一次沒(méi)拿到子組件的問(wèn)題及解決,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-08-08
React無(wú)限滾動(dòng)加載列表組件的封裝實(shí)現(xiàn)
無(wú)限下拉加載技術(shù)是用戶(hù)在大量成塊的內(nèi)容面前一直滾動(dòng)查看,本文主要介紹了React無(wú)限滾動(dòng)加載列表組件的封裝實(shí)現(xiàn),具有一定的參考價(jià)值,感興趣的可以了解一下2023-12-12
React使用ref進(jìn)行訪問(wèn)DOM元素或組件
在 React 里,ref 就像是一個(gè)神奇的小助手,能讓你直接去訪問(wèn) DOM 元素或者組件實(shí)例,下面就跟隨小編一起來(lái)學(xué)習(xí)一下具體的使用方法吧2025-03-03

