React 如何錨定延遲出現的內容
最近在實現 anchor 效果,由於是 SPA 的關係,一般 hash 是沒辦法達成的,因此勢必得自己做一個而且能根據指定資料成功獲取時自動錨定,單純錨定動作可以直接寫一個 function,不須依靠 hook,這次主要針對第一次自動錨定的情境,以下是我第一個版本
function handleAnchor(params){
// ...
}
function useAnchor(params, option){
const { enabled = true } = option
const isScrolled = useRef(false)
useEffect(()=>{
if(!isScrolled.current && enabled){
setTimeout(() => {
handleAnchor(params)
}, 0)
}
}, [enabled])
}
但情況似乎不那麼理想,確實會等 enabled
打開時才執行,但卻是在畫面沒完全畫出來時執行,導致失效。有時渲染工作量稍大一些確實會讓畫面停留幾幀空白,而這幾個等待幀中間只要一有空檔 callback 就會立即塞入執行,但實際上會找不到這幾個元素,白話一點就是過早執行了。
但把時間加大有用嘛?肯定是有的,不過沒辦法確定我的畫面到底會畫多久,固定數字如果超過了也是沒效果,後來找到了 requestIdleCallback
這個 API ,在主執行序閒置時觸發,而我們通常 enabled
開關會是特定資料獲取時會打開,這時內容重新渲染到繪出這個過程執行序是會塞滿的,目前使用上挺良好的,觸發時機點都很理想。
但該死的是這個 API Safari 不支援,雖然有 polyfill 可以抄來用,但實做是使用 setTimeout
,所以其實是有機率失敗的,通常是渲染時間被拉長的時候失敗率最高。而我得知 React 也有處裡這類問題,他們使用了 MessageChannel,這下我個程式就變成這樣了(polyfill 依然留著,畢竟他還是有達到延後效果)
function useAnchor(params, option){
const { enabled = true } = option
const messageChannel = useRef(new MessageChannel())
const isScrolled = useRef(false)
useEffect(()=>{
if(!isScrolled.current && enabled){
messageChannel.current.port1.onmessage = () => handleAnchor(params)
requestIdleCallback(() => {
messageChannel.current.port2.postMessage(undefined)
})
}
}, [enabled])
}
但似乎也只是把失敗機率變更小而已,這依然有可能失敗,我用 Chrome 錄了非常多次執行時序出來看,沒成功的都差一幀左右,於是死馬當活馬醫,我就再延遲一幀
function useAnchor(params, option){
const { enabled = true } = option
const messageChannel = useRef(new MessageChannel())
const isScrolled = useRef(false)
useEffect(()=>{
if(!isScrolled.current && enabled){
messageChannel.current.port1.onmessage = () => handleAnchor(params)
requestIdleCallback(() => {
requestAnimationFrame(() => messageChannel.current.port2.postMessage(undefined))
})
}
}, [enabled])
}
這下總算穩定了,至少我還沒失敗過,壞掉我也沒轍了,不過也只是犧牲一點使用者體驗,應該還好吧 🤡。
但到底還需不需要 MessageChannel
有待商確,或許再掛一個 setTimeout
即可,兩者只有在觸發時間間隔有區別,沒有特別的優先執行順序。