MutationObserver 仅在所有资源已加载时回调

MutationObserver callback only when all resources have loaded

要求

我需要动态更新元素的内容,然后在加载元素及其内容后执行动画。在这种特殊情况下,元素的不透明度为 0,加载内容后,我将其不透明度设置为 1,并使用 CSS 过渡使其淡入。元素的内容有大量图像,在 <img> 标签和子元素的 background-images。非常重要的是,只有在所有图像和背景图像都已加载后才允许开始动画。否则淡入效果会被破坏,因为空的占位符元素会被动画化,其图像内容会在瞬间出现,如下所示。

部分解决方案

为了确定何时加载元素的内容,我使用 MutationObserver 来观察元素并触发一个回调,当检测到变化时改变其不透明度。

var page = document.getElementById( "page" );
var observer = new MutationObserver( function(){
    page.style.opacity = "1"; // begin animation on change
} );
var config = { characterData: true,
               attributes: false,
               childList: false,
               subtree: true };

observer.observe( page, config );
page.innerHTML = newContent; // update content

但是,MutationObserver 本身并不是满足要求的全面解决方案。正如 MDN 页面所述:

The MutationObserver method observe() configures the MutationObserver callback to begin receiving notifications of changes to the DOM that match the given options.

DOM 更改会在子树被修改以及所有会阻止 DOM 被解析的资源被加载时触发。这不包括图像。因此,MutationObservers 将触发回调,而不管内容的图像是否已完全加载。

如果我将 childList 参数设置为 true,在回调中我可以查看每个发生变化的子项并将 load 事件监听器附加到每个 <img> ,跟踪我附加的总数。一旦所有 load 事件都已触发,我就会知道每个图像都已加载。

var page = document.getElementById( "page" ),
    total = 0,
    counter = 0;

var callback = function( mutationsList, observer ){
    for( var mutation of mutationsList ) {
        if ( mutation.type == 'childList' ) {
            Array.prototype.forEach.call( mutation.target.children, function ( child ) {
                if ( child.tagName === "IMG" ) {
                    total++; // keep track of total load events
                    child.addEventListener( 'load', function() {
                        counter++; // keep track of how many events have fired
                        if ( counter >= total ) {
                            page.style.opacity = "1";
                        }
                    }, false );
                }
            } );
        }
    }
}

var observer = new MutationObserver( callback );
var config = { characterData: true,
              attributes: false,
              childList: true,
              subtree: true };

observer.observe( page, config );
page.innerHTML = newContent; // update content

但是,有些带有背景图片的元素也必须加载。由于 load 无法附加到此类元素,因此此解决方案在检测背景图像是否已加载时无用。

问题陈述

如何使用 MutationObserver 确定何时加载动态更新元素的 所有 资源?特别是,我如何使用它来确定隐式依赖资源(如背景图像)是否已加载?

如果 MutationObservers 无法做到这一点,还有什么办法可以做到?使用setTimeout延迟动画不是解决方案。

评论中提出了检测背景图片加载的解决方案。虽然我没有使用 jQuery,因此确切的实现并不合适,但该方法的可转移性足以与 MutationObserver.

一起使用

MutationObserver 必须检测元素及其所有 children 的变化。每一个 child 的变化都必须进行调查。如果 child 是一个图像,那么 load 事件侦听器将附加到它,跟踪我附加的总数。如果 child 不是图像,则检查它是否有背景图像。如果是,则创建一个新图像,如上附加一个 load 事件侦听器,并将图像的 src 设置为 child 的 backgroundImage url.浏览器缓存意味着一旦加载了新图像,背景图像也会加载。

一旦触发的 load 事件数量等于附加事件的总数,我就知道所有图像都已加载。

var page = document.getElementById( "page" ),
    total = 0,
    counter = 0;

var callback = function( mutationsList, observer ){
    for( var mutation of mutationsList ) {
        if ( mutation.type == 'childList' ) {
            Array.prototype.forEach.call( mutation.target.children, function ( child ) {
                var style = child.currentStyle || window.getComputedStyle(child, false);

                if ( child.tagName === "IMG" ) {
                    total++; // keep track of total load events
                    child.addEventListener( 'load', loaded, false );

                } else if ( style.backgroundImage != "none" ) {
                    total++; // keep track of total load events
                    var img = new Image();
                    img.addEventListener( 'load', loaded, false );
                    // extract background image url and add as img src
                    img.src = style.backgroundImage.slice(4, -1).replace(/"/g, "");
                }
            } );
        }
    }
    function loaded() {
        counter++; // keep track of how many events have fired
        if ( counter >= total ) {
            page.style.opacity = "1";
        }
    }
}

var observer = new MutationObserver( callback );
var config = { characterData: true,
            attributes: false,
            childList: true,
            subtree: true };

observer.observe( page, config );
page.innerHTML = newContent; // update content

这个解决方案似乎有点 hack。需要为每种类型的资源实施一个新案例。例如,如果我还需要等到脚本加载完毕,我将不得不识别脚本并将 load 事件侦听器附加到它们,或者如果我必须等待字体加载,我将不得不编写一个案例为了他们。

如果有一个本机函数来确定所有资源何时加载,而不是将事件侦听器附加到所有内容,那就太好了。服务工作者看起来很有趣,但目前我认为 hackish 解决方案是最简单和最可靠的。