跳到主要内容

react 中 img 的 onload

· 阅读需 3 分钟
泥豆君
哇!是泥豆侠,我们没救了

在使用 react 写含 <img /> 组件时,发现该 img 元素的 onload 并不是像想象中那样能够如期执行。

于是对意外进行了反复测验,发现在某些环境下( img 使用缓存到本地资源的图像作为源时)出现的频率较高。

而这导致了我本以 img 元素加载图像作为契机执行的想法成了不可执行事件。

下面的 img 元素并不总能在图像加载后获取到 loaded 样式类
import { useState } from 'react';

export function Img({ src }: {src: string}) {
const [loaded, setLoaded] = useState(false);

const handleLoad = () => {
setLoaded(true);
}


return (
<img src={src}
onload={handleLoad}
className={loaded ? styles.loaded ? style.loading}
/>
);
}

病症

原因是由于在 react 从虚拟 DOM 转化为实际的 DOM 时,先建立 DOM (通过 ReactDOM.render)后进行事件监听(通过 listenToAllSupportedEvents 注册所有可冒泡事件到事件池)及通过 listenToNonDelegatedEvent 未无法冒泡的事件的监听。

而无法冒泡的事件的监听,需要等到整个 DocumentFragment 挂载后才会进行监听,而此时已然错过了由于加载缓存资源而同步触发 load 事件。

备注

目前,已知导致该事件的可能有两个:

  • 事件注册太慢导致滞后性错过了图像元素节点在使用缓存图像源时同步触发 onload
  • 部分浏览器在元素节点使用缓存的图像源时认为无需走全部流程(虽然这个解释很二,但是有可能是 load 事件没有触发的原因之一)
TODO

在根(rootFiber)挂载到 react 程序跟元素的一刻图像资源在加载过程中直接进行了图像 load 事件,而此时并没有注册事件处理(或是由于 DOM 没有渲染完毕,但是不是通过 DocumentFragment 一键挂载的么?)

之后再深入研究源码探索下

开方

既然无法在 img 元素节点 load 时加载状态,那么只能验证其是否加载( img 元素节点的 HTMLImageElement.complete 属性)而判定元素是否已经触发load(但这样会增加额外的工作量)。

修复后的简单示意
import React, { useState, useRef, useEffect } from 'react';

export function Img({ src }: { src: string }) {
const imgRef = useRef<HTMLImageElement>(null);
const [isLoaded, setIsLoaded] = useState(false);

useEffect(() => {
const img = imgRef.current;
if (!img) return; // 未加载
// 在刚初始化完成后图像资源已经加载完毕,那么直接更新状态而无需对 load 事件进行处理
if (img.complete && img.naturalHeight !== 0) {
setIsLoaded(true);
return;
}
const handleLoad = () => setIsLoaded(true); // 处理句柄
img.addEventListener('load', handleLoad); // 注册 load 事件
// 清理监听事件
return () => img.removeEventListener('load', handleLoad);
}, [src]);

return (
<img
ref={imgRef}
alt={'一张图片'}
src={src}
className={isLoaded ? 'block' : 'none'}
/>
);
}

多使用了 useEffectuseRef Hooks ,在性能上肯定有轻微的影响,但是这确实处理该问题的良策。