JavaScript - 所有图片加载完成后执行

55

在阅读了其他人的问题之后,我想

window.onload=...

我希望有人能回答我的问题。我已经尝试过这个方法,但它会在页面加载后立即执行代码(而不是在图片加载后执行)。

如果图像来自 CDN 并且不是相对路径,是否有任何区别?

有人知道解决方案吗?(我没有使用 jQuery)


1
你想要达到什么目的?也许有比预加载图像更高效的方法。 - Alp
10个回答

89

想要一个一句话描述吗?

Promise.all(Array.from(document.images).filter(img => !img.complete).map(img => new Promise(resolve => { img.onload = img.onerror = resolve; }))).then(() => {
    console.log('images finished loading');
});

相当向后兼容,即使在Firefox 52和Chrome 49(Windows XP时代)中也能工作。但不支持IE11。

如果要缩小图像列表,请使用 document.querySelectorAll(...) 替换 document.images

它使用 onload onerror 以简便为主。如果页面上这些 img 元素的处理程序也在其他地方设置了(不太可能,但无论如何),这可能会与页面上的其他代码发生冲突。如果您不确定页面是否使用它们并希望安全,请将部分 img.onload = img.onerror = resolve; 替换为更长的一部分: img.addEventListener('load', resolve); img.addEventListener('error', resolve);

它还没有测试所有图像是否成功加载(即没有损坏的图像)。如果需要此功能,则可以使用以下更高级的代码:

Promise.all(Array.from(document.images).map(img => {
    if (img.complete)
        return Promise.resolve(img.naturalHeight !== 0);
    return new Promise(resolve => {
        img.addEventListener('load', () => resolve(true));
        img.addEventListener('error', () => resolve(false));
    });
})).then(results => {
    if (results.every(res => res))
        console.log('all images loaded successfully');
    else
        console.log('some images failed to load, all finished loading');
});

它会等待所有图片加载或加载失败。

如果你想尽早失败,使用第一张损坏的图片:

Promise.all(Array.from(document.images).map(img => {
    if (img.complete)
        if (img.naturalHeight !== 0)
            return Promise.resolve();
        else
            return Promise.reject(img);
    return new Promise((resolve, reject) => {
        img.addEventListener('load', resolve);
        img.addEventListener('error', () => reject(img));
    });
})).then(() => {
    console.log('all images loaded successfully');
}, badImg => {
    console.log('some image failed to load, others may still be loading');
    console.log('first broken image:', badImg);
});

最近的两个代码块使用naturalHeight来检测已经加载的图像中是否有损坏的图像。这种方法通常有效,但有一些缺点:当图像URL通过CSS content属性设置,并且图像是未指定尺寸的SVG时,据说该方法不起作用。 如果是这种情况,您需要重构代码,以便在图像开始加载之前设置事件处理程序。 这可以通过在HTML中直接指定onloadonerror或通过在JavaScript中创建img元素来完成。另一种方法是在HTML中将src设置为data-src,并在附加处理程序后执行img.src = img.dataset.src


6
非常好的回答。我发现一个错误的边缘情况:在Firefox中,可能会出现一个<img>处于completed === false状态的无限期问题,因为从未尝试加载图像,因为协议是未知的。例如,<img src="aaaa:">。您的代码仍然有效,因为会触发onerror,但是当您动态运行代码时,它会失败。我在这里制作了一个演示(http://jsfiddle.net/f9ku7jn4/)。也许是Firefox的一个错误? - phil294
@phil294,听起来确实是个bug,应该向Bugzilla报告。 - user
1
最佳答案在这里。支持背景图片的方式将是一个加分项。 - BenMorel
请问您能否解释一下为什么 img.onload = img.onerror = resolve 看起来和 if( img.onload && img.onerror) resolve() 的效果相同? - user3746571
如何在next.js中使其正常工作?@user - undefined

71

以下是一种适用于现代浏览器的快速解决方案:

var imgs = document.images,
    len = imgs.length,
    counter = 0;

[].forEach.call( imgs, function( img ) {
    if(img.complete)
      incrementCounter();
    else
      img.addEventListener( 'load', incrementCounter, false );
} );

function incrementCounter() {
    counter++;
    if ( counter === len ) {
        console.log( 'All images loaded!' );
    }
}

当所有图片都加载完成后,控制台会显示"All images loaded!"。

这段代码的作用:

  • 从文档中将所有图片加载到一个变量中
  • 循环遍历这些图片
  • 为每个图片添加"load"事件监听器,以运行incrementCounter函数
  • incrementCounter函数将增加计数器
  • 如果计数器达到了图片数量,那么表示它们全部已经被加载完成

以跨浏览器的方式实现这段代码并不是特别难,只是这样写更加清晰。


6
两点提示:如果您的脚本加载较晚,可能会错过一些事件,导致脚本无法完成。此外,您的浏览器可能不会加载最初由 CSS 隐藏的所有图像。 - Micros
1
@TheBndr,在实现上面的答案时要记住一些事情。 - Micros
1
@Micros 我已经为你的第一个问题添加了修复。 - Jens
1
如果某个图像损坏,这段代码就不会触发。你还应该处理“error”事件。 - user
@user:你对处理损坏的图片有什么建议?在我的情况下,我正在监控加载的图像数量,并相应地显示进度预加载器。当所有图像都加载完毕时,我隐藏预加载器并在主内容中进行动画处理。如果其中一个图像损坏了,我不确定该怎么办。 - bytrangle
显示剩余3条评论

18

Promise模式将以最佳方式解决此问题。我参考了一个开源库when.js来解决所有图像加载的问题。

function loadImage (src) {
    var deferred = when.defer(),
        img = document.createElement('img');
    img.onload = function () { 
        deferred.resolve(img); 
    };
    img.onerror = function () { 
        deferred.reject(new Error('Image not found: ' + src));
    };
    img.src = src;

    // Return only the promise, so that the caller cannot
    // resolve, reject, or otherwise muck with the original deferred.
    return deferred.promise;
}

function loadImages(srcs) {
    // srcs = array of image src urls

    // Array to hold deferred for each image being loaded
    var deferreds = [];

    // Call loadImage for each src, and push the returned deferred
    // onto the deferreds array
    for(var i = 0, len = srcs.length; i < len; i++) {
        deferreds.push(loadImage(srcs[i]));

        // NOTE: We could push only the promise, but since this array never
        // leaves the loadImages function, it's ok to push the whole
        // deferred.  No one can gain access to them.
        // However, if this array were exposed (e.g. via return value),
        // it would be better to push only the promise.
    }

    // Return a new promise that will resolve only when all the
    // promises in deferreds have resolved.
    // NOTE: when.all returns only a promise, not a deferred, so
    // this is safe to expose to the caller.
    return when.all(deferreds);
}

loadImages(imageSrcArray).then(
    function gotEm(imageArray) {
        doFancyStuffWithImages(imageArray);
        return imageArray.length;
    },
    function doh(err) {
        handleError(err);
    }
).then(
    function shout (count) {
        // This will happen after gotEm() and count is the value
        // returned by gotEm()
        alert('see my new ' + count + ' images?');
    }
);

3
这应该是被接受的答案。现代化解决方案,运行良好。赞扬Ajay。 - TaylorMac
更好的解决方案,应该被接受为最佳答案。 - Anvar Pk

2
使用 window.onload 不起作用,因为它只在页面加载完毕后触发,然而图片并不包括在这个“已加载”的定义中。
一般的解决方案是使用 ImagesLoaded jQuery 插件。
如果你不想使用 jQuery,你至少可以尝试将此插件转换为纯 JavaScript。它有 93 行重要的代码和良好的注释,应该不难完成。

1
您可以在图像上使用onload事件,回调一个处理函数...关于如何处理所有图像是否已加载,我不确定以下任何机制是否有效:
编写一个函数来计算onload被调用的图像数量,如果这个数量等于页面上所有图像的总数,则进行必要的处理。

0

我一直在寻找这样的东西,如果您不介意使用 setInterval,那么这段代码非常简单明了。在我的情况下,我可以使用 setInterval,因为它可能只会运行4-5次。

const interval = setInterval(() => {
    const allImagesLoaded = [...document.querySelectorAll('img')]
      .map(x => x.complete)
      .indexOf(false) === -1;
    if (allImagesLoaded) {
      window.print();
      clearInterval(interval);
    }
  }, 500);

0
这是对Florian Margaine上述方法的轻微变化。这考虑到图像中可能存在的损坏链接。如果您仍然需要使用这些图像(例如,在加载时使用它们的高度来计算另一个元素的高度),则可以使用此方法实现。
let imgs = document.querySelectorAll("img"),
counter = 0;

// Loop through images/check if it's loaded/if it is, increment the counter/else listen for when it does load and increment then
imgs.forEach((img) => (img.complete ? incrementCounter() : img.addEventListener("load", incrementCounter, false)));

function incrementCounter() {
    counter++;
    // If at least 1 image is loaded, do something
    if (counter !== 0) {
        calculateHeight();
    }
}

0
 <title>Pre Loading...</title>
 </head>

 <style type="text/css" media="screen"> html, body{ margin:0;
 padding:0; overflow:auto; }
 #loading{ position:fixed; width:100%; height:100%; position:absolute; z-index:1; ackground:white url(loader.gif) no-repeat center; }**
 </style>

 <script> function loaded(){
 document.getElementById("loading").style.visibility = "hidden"; }
 </script>

 <body onload="loaded();"> <div id="loading"></div>

 <img id="img" src="avatar8.jpg" title="AVATAR" alt="Picture of Avatar
 movie" />


 </body>

-2

我正要建议和Baz1nga说的一样。

另外,还有一个可能不是那么百分之百可靠但更容易维护的选择,就是选择最重要/最大的图像,并仅对该图像附加onload事件。 这里的优点是,如果您以后向页面添加更多图像,则需要更改的代码较少。


-4

这个非常好用:

$(function() {
 $(window).bind("load", function() {
    // code here
 });
});

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接