图片加载场景下,如何用设计模式避免晚返回Promise覆盖先完成Promise?
嘿,这个问题在异步加载场景里真的太常见了!尤其是用户快速点击多个缩略图时,慢的请求后完成就会把快的结果直接覆盖掉,导致gallery显示的不是用户最后点击的那张图对吧?我来给你几个实用的解决思路,都是在大型Three.js应用里验证过的:
方法1:用AbortController取消旧请求(推荐)
现代浏览器都支持AbortController,它能真正取消正在进行的图片加载请求,不仅能解决覆盖问题,还能节省带宽(对大型Three.js应用里的高清纹理/图片特别重要)。核心思路是:每次用户点击新缩略图时,先取消上一个还在加载的请求,再发起新请求。
示例代码:
// 维护当前的请求控制器 let currentAbortController = null; function loadImage(url) { // 先取消之前未完成的请求 if (currentAbortController) { currentAbortController.abort(); } // 创建新的控制器 currentAbortController = new AbortController(); const signal = currentAbortController.signal; return new Promise((resolve, reject) => { const img = new Image(); img.onload = () => { resolve(img); // 请求完成后清空控制器,避免内存泄漏 currentAbortController = null; }; img.onerror = (err) => { reject(err); currentAbortController = null; }; // 绑定中止信号到图片加载 img.src = url; signal.addEventListener('abort', () => { reject(new Error('请求已被取消')); img.src = ''; // 清空图片加载状态 }); }); } // 缩略图点击事件处理 document.querySelectorAll('.thumbnail').forEach(thumb => { thumb.addEventListener('click', () => { const largeImgUrl = thumb.dataset.largeUrl; loadImage(largeImgUrl) .then(img => { // 更新gallery显示 const gallery = document.getElementById('gallery'); gallery.innerHTML = ''; gallery.appendChild(img); }) .catch(err => { // 忽略取消请求的错误,只处理真正的加载失败 if (err.name !== 'AbortError') { console.error('图片加载失败:', err); } }); }); });
方法2:用请求ID过滤旧结果(兼容旧浏览器)
如果你的应用需要兼容不支持AbortController的旧浏览器,这个方法更合适。核心思路是:给每个请求分配一个唯一ID,只有当完成的请求ID和当前最新的ID一致时,才更新UI。
示例代码:
// 维护最新的请求ID let latestRequestId = 0; function loadImage(url, requestId) { return new Promise((resolve, reject) => { const img = new Image(); img.onload = () => resolve({ img, requestId }); img.onerror = reject; img.src = url; }); } // 缩略图点击事件处理 document.querySelectorAll('.thumbnail').forEach(thumb => { thumb.addEventListener('click', () => { // 生成当前请求的唯一ID const currentRequestId = ++latestRequestId; const largeImgUrl = thumb.dataset.largeUrl; loadImage(largeImgUrl, currentRequestId) .then(({ img, requestId }) => { // 只有当前请求是最新的才更新UI if (requestId === latestRequestId) { const gallery = document.getElementById('gallery'); gallery.innerHTML = ''; gallery.appendChild(img); } }) .catch(err => console.error('图片加载失败:', err)); }); });
方法3:封装加载管理器(适合大型应用)
如果你的Three.js应用有大量图片/纹理加载需求,可以封装一个专门的加载管理器,统一处理请求的取消和优先级,让代码更整洁可维护。
示例代码:
class ImageLoadManager { constructor() { this.currentRequest = null; } load(url) { // 取消上一个请求 if (this.currentRequest?.controller) { this.currentRequest.controller.abort(); } const controller = new AbortController(); const signal = controller.signal; const promise = new Promise((resolve, reject) => { const img = new Image(); img.onload = () => { if (!signal.aborted) { resolve(img); } }; img.onerror = reject; img.src = url; signal.addEventListener('abort', () => { reject(new Error('请求已取消')); img.src = ''; }); }); this.currentRequest = { controller, promise }; return promise; } } // 全局实例化管理器 const imageManager = new ImageLoadManager(); // 缩略图点击事件处理 document.querySelectorAll('.thumbnail').forEach(thumb => { thumb.addEventListener('click', () => { const largeImgUrl = thumb.dataset.largeUrl; imageManager.load(largeImgUrl) .then(img => { const gallery = document.getElementById('gallery'); gallery.innerHTML = ''; gallery.appendChild(img); }) .catch(err => { if (err.name !== 'AbortError') { console.error('图片加载失败:', err); } }); }); });
选择建议
- 优先用方法1,既能解决覆盖问题,又能节省带宽,适合现代浏览器环境下的大型Three.js应用;
- 如果需要兼容旧浏览器,选方法2,实现简单且兼容性好;
- 大型应用推荐用方法3,统一管理加载逻辑,便于扩展(比如添加加载状态、错误重试等)。
内容的提问来源于stack exchange,提问作者DTC
相关产品推荐
相关产品推荐

