【发布时间】:2020-01-26 20:03:38
【问题描述】:
场景:
我们有一个MutationObserver 处理函数handler。
在handler 中,我们做了一些DOM 操作,会再次触发handler。从概念上讲,我们将有一个可重入的handler 调用。除了MutationObserver 不在线程内运行,它会在handler 已经完成执行后触发。
因此,handler 将自行触发,但通过异步队列而非线程内触发。 JS 调试器似乎知道这一点,它将自己作为调用堆栈中的异步祖先(即使用 Chrome)。
为了实现一些有效的事件去抖动,我们需要检测相同的;也就是说,如果 handler 是由于自身触发的更改而被调用的。
那怎么办?
mutationObserver=new MutationObserver(handler);
mutationObserver.observe(window.document,{
attributes:true,
characterData:true,
childList:true,
subtree:true
});
var isHandling;
function handler(){
console.log('handler');
// The test below won't work, as the re-entrant call
// is placed out-of-sync, after isHandling has been reset
if(isHandling){
console.log('Re-entry!');
// Throttle/debounce and completely different handling logic
return;
}
isHandling=true;
// Trigger a MutationObserver change
setTimeout(function(){
// The below condition should not be here, I added it just to not clog the
// console by avoiding first-level recursion: if we always set class=bar,
// handler will trigger itself right here indefinitely. But this can be
// avoided by disabling the MutationObserver while handling.
if(document.getElementById('foo').getAttribute('class')!='bar'){
document.getElementById('foo').setAttribute('class','bar');
}
},0);
isHandling=false;
}
// NOTE: THE CODE BELOW IS IN THE OBSERVED CONTENT, I CANNOT CHANGE THE CODE BELOW DIRECTLY, THAT'S WHY I USE THE OBSERVER IN THE FIRST PLACE
// Trigger a MutationObserver change
setTimeout(function(){
document.getElementById('asd').setAttribute('class','something');
},0);
document.getElementById('foo').addEventListener('webkitTransitionEnd',animend);
document.getElementById('foo').addEventListener('mozTransitionEnd',animend);
function animend(){
console.log('animend');
this.setAttribute('class','bar-final');
}
#foo {
width:0px;
background:red;
transition: all 1s;
height:20px;
}
#foo.bar {
width:100px;
transition: width 1s;
}
#foo.bar-final {
width:200px;
background:green;
transition:none;
}
<div id="foo" ontransitionend="animend"></div>
<div id="asd"></div>
注意 我们的用例在这里包含两个组件;我们称之为contents,它是任何普通的Web 应用程序,具有许多UI 组件和界面。还有一个叠加层,它是观察内容变化的组件,并可能自己做变化。
一个简单的想法是不够的,只是在处理时禁用MutationObserver;或者,假设对handler 的每一秒调用都是递归的;这在上面说明的animationend 事件的情况下不起作用:contents 可以有处理程序,而这些处理程序又可以触发异步操作。两个最受欢迎的此类问题是:onanimationend/oneventend、onscroll。
所以仅仅检测直接(第一次调用)递归的想法是不够的,我们需要在调试器中完全等同于调用堆栈视图:一种判断是否调用的方法(无论以后有多少异步调用) 是它自己的后代。
因此,这个问题不仅限于MutationObserver,因为它必然涉及一种通用方法来检测调用树中自身的异步调用后代。你可以用任何异步事件替换MutationObserver,真的。
上面例子的解释:在例子中,当#foo不是.bar时,mutationobserver在#foo上触发bar动画。但是,contents 有一个 transitionend 处理程序,它将 #foo 设置为 .bar-final,这会触发一个恶性的自递归链。我们想放弃对#foo.bar-final 更改的反应,方法是检测到这是我们自己操作的结果(以#foo.bar 开始动画)。
【问题讨论】:
-
为什么不直接使用高阶函数去抖动呢?您不需要在您创建的每个函数中实现去抖动逻辑。
-
严格来说,在 JavaScript 中不存在重入之类的东西。你看到的只是另一个事件循环周期,特别是微任务队列周期。 JavaScript 中没有特殊机制,因此您只需在元素上设置一个告示扩展属性或使用
new WeakSet()添加更改的元素并稍后检查。 -
@VLAZ - 我不太确定我在这里关注你。
-
@wOxxOm - 如果我理解正确你在说什么 - 我需要知道一个元素是因为
handler而改变,还是因为它而改变,或者两者兼而有之。您所说的似乎并没有增加MutationObserver已经做的事情:准备一批更改以处理异步;我需要的不仅仅是这些,我需要知道这些修改的来源。我需要的是调试器似乎完美无缺:确定handler is on the async call stack. -
@Kaiido - 所以我需要区分带内变化和带外变化(overlay 起源的变化)。我能想到的最好的方法是使用 2 个
MutationObservers 和 2 个回调,它们在观察时相互断开连接(因此一个观察者的处理程序将在处理时禁用另一个观察者)。但是我想知道是否有更通用的方法来执行此操作,而不管异步事件生成器如何(我们可以在这里用requestAnimationFrame或onanimationend替换MutationObserver,它仍然是同一个问题:如何判断是否处理程序异步触发自身。
标签: javascript mutation-observers