直接new Image()无法可靠处理加载失败,因onerror触发后src重赋值不再生效,错误状态被锁定;需用Proxy隔离状态、每次新建实例并支持可控重试与fallback。

为什么直接 new Image() 无法可靠处理图片加载失败?
浏览器对 img 元素的 onerror 触发有严格限制:一旦触发,后续即使 src 改为有效地址,也不会再次尝试加载(除非手动重置 src 为空再赋值)。原生 Image 对象同样受此机制影响,且错误回调里无法直接“重试”——因为 src 已设过,状态已卡在 error。这不是 bug,是规范行为。
代理模式在这里的作用,不是封装逻辑,而是**隔离加载状态与 DOM 渲染**,让每次加载都像一次全新请求,同时把重试、降级、缓存等策略收口到代理实例中。
用 Proxy 封装 Image 实例并支持自动重试
核心是拦截 src 赋值,内部创建新 Image 实例发起请求,并暴露 load / error 链式响应。关键点在于:每次 src 变更都抛弃旧实例,不复用。
- 不监听原生
onerror后直接改src——这会失效;而是在 error 回调里主动构造新Image实例重试 - 重试次数需显式控制,避免无限循环;建议默认 2 次,第 1 次失败后等 200ms 再试
- 错误时可 fallback 到备用 URL(如 CDN 备源、本地占位图),但 fallback 地址必须和原地址有明确区分,否则会陷入死循环
- 避免在 proxy 的
set中直接执行异步操作;应返回 Promise 并由使用者 await 或链式处理
function createImageProxy() {
let currentImg = null;
let currentSrc = '';
return new Proxy(
{},
{
set(target, prop, value) {
if (prop === 'src') {
currentSrc = value;
currentImg?.removeEventListener('load', handleLoad);
currentImg?.removeEventListener('error', handleError);
currentImg = new Image();
currentImg.addEventListener('load', handleLoad);
currentImg.addEventListener('error', handleError);
currentImg.src = value;
}
return true;
},
get(target, prop) {
if (prop === 'src') return currentSrc;
if (prop === 'then') return (onFulfill, onReject) => {
// 暴露 Promise 接口便于链式使用
const p = new Promise((resolve, reject) => {
const handleLoad = () => resolve(currentImg);
const handleError = () => reject(new Error(`Failed to load ${currentSrc}`));
currentImg?.addEventListener('load', handleLoad);
currentImg?.addEventListener('error', handleError);
});
return p.then(onFulfill, onReject);
};
return undefined;
}
}
);
}
预加载阶段如何避免重复请求与内存泄漏?
预加载 ≠ 立即插入 DOM。很多场景下你只是想提前触发 HTTP 请求,等真正需要时再把已加载的 Image 实例塞进 标签。这时候必须管理好实例生命周期。
- 每个预加载任务应绑定唯一 key(如 URL + 尺寸参数),用 Map 缓存
Image实例,避免同一资源多次 fetch - 缓存的实例一旦被消费(比如 append 到页面),就从 Map 中删除,否则
Image对象长期持有会阻碍 GC - 如果预加载后长时间未使用(比如 >30s),可主动调用
currentImg?.src = ''清空,释放连接(尤其对 WebP/AVIF 等大图) - 不要在预加载时设置
crossOrigin除非必要——它会禁用部分浏览器缓存,且跨域失败时 error 不会携带 status code
错误重构时最容易忽略的三个细节
所谓“重构”,是指加载失败后动态替换为其他内容(如 base64 占位图、文字提示、甚至 SVG fallback),而不是简单换 src。这里埋着几个静默失败点:
-
img.onerror在某些 iOS 版本中不会触发 404,只会触发 0 状态码的 error;需结合img.naturalWidth === 0做二次判断 - 用
canvas.toDataURL()生成 fallback 图片时,若原图跨域且未声明crossOrigin="anonymous",会触发 canvas 污染异常,且该异常无法 try/catch,只能靠canvas.width是否为 0 来间接识别 - 服务端返回 200 但 body 是 HTML(如 Nginx 默认 404 页面),此时
img仍会触发 load 事件——要检查img.naturalHeight是否为 0 或宽高比是否异常,才能识别“假成功”
代理本身不解决所有问题,但它把状态判断、重试时机、fallback 分发这些散落在各处的逻辑,收束到一个可测试、可替换的对象里。真正难的不是写代理,而是定义清楚:什么算失败?重试间隔怎么定?fallback 的优先级链怎么组织?这些决策点,比语法更重要。










