最近在開發 side project,運用到 GSAP 製作網頁動畫,其中 ScrollTrigger 提供好用的 markers key,可以標記出動畫會在網頁卷軸接觸到哪個位置時開始、結束,但我的畫面上始終有個圖片動畫的 markers 標記不在我設定的地方,可其他文字動畫都在對的地方,後來爬了文才體悟到「圖片是非同步載入的,而動畫在圖片完成載入前就先架完,自然抓不到圖片的正確位置」,於是就往「如何在 React 確認圖片完成載入才執行後續動作」去找資料,找到了作者 Alejandro Martinez 自製確認圖片完成載入的 hook ,非常讚,以下筆記
在使用自製 hook 前,先了解 image element 有一個特別的屬性:complete
。
它的值為布林值,true 表示圖片完全載入,反之為 false。
<img>
會被視為圖片完全載入:src
或 srcset
attribute 不存在srcset
attribute 不存在,src
attribute 為空值 ""
跟原作者的寫法幾乎是一模一樣,只是對方是用 TS,我改成 JS,並做了一些註解
import { useState, useEffect } from 'react';
export const useOnLoadImages = (ref) => {
// 初始 images 載入狀態為 false
const [status, setStatus] = useState(false);
useEffect(() => {
// 更新 images 載入狀態的函式,接收 images 陣列,並在裡面使用 .complete property,來檢查 image 是否「完成載入」還沒會是 false,只有當收到的 images 陣列「都(.every)」完成載入,值才會是 true,set 不一樣的值觸發 re-render,最後傳出去的值變成 true,使用這個 hook 的元件也會收到 true 值
const updateStatus = (images) => {
setStatus(
images.map((image) => image.complete).every((item) => item === true)
);
};
// ref 不存在就可以直接 return 掉
if (!ref.current) return;
// 抓 ref 裡面所有的 img,並將 NodeList 改成陣列型別
const imagesLoaded = Array.from(ref.current.querySelectorAll('img'));
// 如果陣列為空,沒有任何 img 要載入,也可以說是確認 images 載入完成,更新 status 為 true
if (imagesLoaded.length === 0) {
setStatus(true);
return;
}
// 在每個圖片上面綁監聽器,監聽 load 跟 error 事件,這兩個事件發生都代表著 .complete 為 true,就調用 updateStatus 更新現在整體的圖片載入狀況
// 監聽器第三個參數,當 once: true,代表這個監聽器只會執行一次,就會自動移除
imagesLoaded.forEach((image) => {
image.addEventListener('load', () => updateStatus(imagesLoaded), {once: true,});
image.addEventListener('error', () => updateStatus(imagesLoaded), {once: true,});
});
}, [ref]);
// 回傳 images 載入狀態
return status;
};
我在第 12 行加上一個 if 判斷式:只有圖片載入完成才上動畫,搭配 useEffect 的特性,即便是 initial render,markers 也會在正確的位置出現 🥳
// 記得引入
import { useEffect, useRef } from 'react';
import { useOnLoadImages } from '../hooks/useOnLoadImages';
function Part1() {
// 只需要確認 part1 內的圖片元素即可
const part1 = useRef();
const isImagesLoaded = useOnLoadImages(part1);
useEffect(() => {
// 只有圖片載入完成才上動畫
if (isImagesLoaded) {
// 設計的動畫(略)..
// ..
}
// isImagesLoaded 值有更動就會觸發此 useEffect
}, [isImagesLoaded]);
return (
<div ref={part1}>
<img src={ooo} alt="ooo" className="ooo" />
<img src={xxx} alt="xxx" className="xxx" />
</div>
);
}
以上是我的使用情境,也很歡迎大家點入原作者的文章看他 demo 另一個使用情境
Scrolltrigger pins in wrong position on intitial load (React app) #comment-133157
Mozilla - HTMLImageElement: complete property
Alejandro Martinez - How to detect images loaded in React
響應式圖片 Responsive Images,img srcset 的用法
有任何想法歡迎提出交流:)
.
筆者小記:開發 side project 常伴隨著精彩的踩坑之旅 ε-(´∀`; )