一个setTimeout解决IE神坑的故事

120 阅读2分钟

前言

最近写(魔改)了一个前端水印的插件,为了加强对信息保护的力度,利用MutationObserver来监听DOM节点的变化,一旦检测到水印节点被删除,马上就会重新创建一个水印节点覆盖回原来的地方,防止数据在无水印的情况下被截图。

这个方案在Chrome上运行得好好的,没想到在IE 11上却碰到了问题:如果用户使用Dev Tools暴力删除了水印Element, MutationObserver的回调函数明明执行了却不会重新创建到水印,见鬼了!

检索了好久都没发现答案,可能真的太少人会碰到这种奇葩问题了,最后通过一步一步分析试错,终于解决了问题。因为实在是个小众需求,无此需要的看官可以关闭页面了,毕竟都快2023年了还有几个项目要兼容IE呢?

试探与解决方案

试探部分(可忽略,直接看下方解决方法)

有关代码如下,create函数为创建水印部分,里面涵盖了创建水印element并appendChild的部分。

const callback = (mutationsList) => {
    let isChangeWaterMark = false;
    mutationsList.forEach((item) => {
        if (item.target === this.watermark) {
            console.log("the watermark dom is changed!");
            isChangeWaterMark = true;
            return;
        }
        if (item.removedNodes.length) {
            for (var i = 0; i < item.removedNodes.length; i++) {
                if (item.removedNodes[i] === this.watermark) {
                    console.log("the watermark dom is removed!");
                    isChangeWaterMark = true;
                    if (this.onWatermarkRemove && typeof this.onWatermarkRemove) {
                        this.onWatermarkRemove();
                    }
                }
            }
        }
    });
    if (isChangeWaterMark) {
        this.create();
    }
};
const MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver;
const observer = new MutationObserver(callback);
observer.observe(targetNode, config);

首先,由于在Chrome上是能够执行水印节点重新创建的,而且在控制台上逐步回显了回调执行信息。初步排除是MutationObservercallback有问题,在IE 11上是的确可以监听到DOM节点被remove了。

接着在源码中执行如下代码 (由于IE上Element没有remove方法)

var dom = document.getElementById("watermark");
dom.parentNode.removeChild(dom);

观察到在dom被删除后,MutationObserver的回调函数是会重新插入水印的。证明IE11上callback是会执行的。

那么用代码删除不会出问题,只能推测是用Dev Tools直接删除水印dom这一行为是导致水印节点不会重新创建的元凶了。

重试上述行为,发现console.log("the watermark dom is removed!");有log,亦即callback成功执行。

试着在create函数的最后插入如下语句

function create(){
    // origin
    document.body.appendChild(watermarkDom);
    // latest inserted
    console.log(document.getElementById("watermark"))
    // return a DOM Element
}

观察到#watermark在函数执行后是成功插入到document.body的,但是页面上不但找不到,在控制台尝试getElementById返回null,见鬼了,成功插入的dom消失了...

试探一番,看看在插入1秒后(也即观察页面的时候)水印在不在:

setTimeout(()=>{
    console.log(document.getElementById("watermark"))  
},1000);
// null

然后把上边的1000改为0,发现也是null

解决

也就是说,即便是MutationObserver的callback明明已经插入了#watermark,在事件队列的末尾添加执行document.getElementById("watermark")方法也会找不到。

由于在chrome等其他浏览器上都没重现这个问题,推理在IE上面使用Dev Tools删除一个DOM节点时,在MutationObserver的回调函数以后事件队列里面还有对目标DOM节点的删除事件,所以在回调函数里面插入的新水印在setTimeout(0)以后就消失了。

为了验证这个猜想,把MutationObserver的回调函数里面把appendChild的部分修改一番,把插入DOM的执行挪到事件队列的末尾:

function create(){
    setTimeout(()=>{
        document.body.appendChild(watermarkDom);
    },0);
}

发现果然成功了!在页面上见到了水印!

至于原因是不是

IE上面使用Dev Tools删除一个DOM节点时,在MutationObserver的回调函数以后事件队列里面还有对目标DOM节点的删除事件

我也不确定,也懒得研究了,就让IE滚粗吧!

完结撒花🎉