React 如何錨定延遲出現的內容

Javascript 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 即可,兩者只有在觸發時間間隔有區別,沒有特別的優先執行順序。