【问题标题】:Check for async function re-entry in JS检查 JS 中的异步函数重新进入
【发布时间】: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/oneventendonscroll

所以仅仅检测直接(第一次调用)递归的想法是不够的,我们需要在调试器中完全等同于调用堆栈视图:一种判断是否调用的方法(无论以后有多少异步调用) 是它自己的后代。

因此,这个问题不仅限于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 个回调,它们在观察时相互断开连接(因此一个观察者的处理程序将在处理时禁用另一个观察者)。但是我想知道是否有更通用的方法来执行此操作,而不管异步事件生成器如何(我们可以在这里用requestAnimationFrameonanimationend 替换MutationObserver,它仍然是同一个问题:如何判断是否处理程序异步触发自身。

标签: javascript mutation-observers


【解决方案1】:

一种可能的解决方法是在触发一个突变时停止突变观察者

mutationObserver=new MutationObserver(handler);
mutationObserver.observe(window.document,{
    attributes:true,
    characterData:true,
    childList:true,
    subtree:true
});

//  Trigger a MutationObserver change
document.getElementById('foo').setAttribute('class','bar');
document.getElementById('foo').setAttribute('class','');

function handler(){
    console.log('Modification happend')

        mutationObserver.disconnect();
    //  Trigger a MutationObserver change
    document.getElementById('foo').setAttribute('class','bar');
    document.getElementById('foo').setAttribute('class','');

    mutationObserver.observe(window.document,{
    attributes:true,
    characterData:true,
    childList:true,
    subtree:true
});
}

查看 JS 小提琴

https://jsfiddle.net/tarunlalwani/8kf6t2oh/2/

【讨论】:

  • 谢谢,我们确实考虑到了这一点(见问题 cmets)。但是我们希望有一种解决方案可以通用地检测处理程序何时触发自身;因为在我们的问题中,MutationObserver 可以替换为requestAnimationFrameonanimationend,甚至setTimeout... 结果是一样的:创建异步任务流。因此,仅分离MutationObserver 只会产生解决问题的外观。示例:如果处理程序在具有 onanimationend 处理程序的元素上设置动画 CSS 类,这反过来又会触发突变……您会得到图片。
  • 如果使用 API 来操作任何类型的 UI 组件,它会变得更加复杂:其中很多使用 setTimeout(...,0) 来进行操作;在这种情况下,我再次留下一个异步调用,禁用/启用观察者将无济于事,因为执行是异步调度的。但是查看浏览器中的堆栈跟踪,这很容易检测到,因为 JS 仍然将 handler 跟踪为异步调用祖先。
  • 不要通过浏览器堆栈跟踪,因为浏览器通常具有增强的调试信息,您无法在 javascript 本身中获得
  • 我知道,我希望有某种结构,例如我可以使用的“异步范围”...我很确定 await 需要这样的东西才能工作...这将允许设置一个在未来的异步任务中可用的变量。我知道这是一种边缘用途,所以我只是在寻找可能知道诀窍的人:)
【解决方案2】:

我过去这样做的方法是创建信号量来标记异步函数何时已被调用并等待事件循环上的执行。

这里是 requestAnimationFrame 的一个简单示例

import raf from 'raf'
import State from './state'

let rafID

function delayedNotify () {
  rafID = null
  State.notify()
}

export default function rafUpdateBatcher () {
  if (rafID) return // prevent multiple request animation frame callbacks
  rafID = raf(delayedNotify)
}

在这种情况下,一旦您调用该函数,所有未来的调用都将被忽略,直到第一个调用被执行。将其视为异步节流函数

对于更复杂的场景,另一个解决方案可能是这个项目 https://github.com/zeit/async-sema

【讨论】:

  • 好吧,但我没有一个“结束”事件,它是暂时的并且没有链接到调用堆栈。我也不想解雇在一个时间范围内发生的所有事件。我只想解雇(或以不同方式处理)作为处理程序中我自己的操作的异步递归而发生的事件。不是由底层观察到的应用程序导致的任何其他事件。
  • 然后,您需要尝试在事件完成时提升信号量,使用 setTimeout 0 或不起作用尝试在 HTML 元素上设置数据属性并使用 mutationObserver 检测它何时删除
【解决方案3】:

根据我从阅读您的 cmets 中收集到的信息,如果操作 A 异步触发了操作 B,您希望能够知道操作 A 在哪里完成(通常,不仅仅是在突变观察者中)。我不认为 JavaScript 中内置了任何技巧来做到这一点,就像你正在寻找的那样,但是,如果你确切地知道你的 JavaScript 是如何工作的,你就可以跟踪这些信息。 JavaScript 中的作业队列根据定义是 FIFO,事件循环队列也以这种方式工作。这意味着您可以在执行触发事件的操作的同时将与特定事件相对应的信息存储在数组中,并确信它们的处理顺序与数组相同。这是您的突变观察者的示例。

const
    foo = document.getElementById('foo'),
    mutationQueue = [];

function handler(){
    console.log('handler');
    
    const isHandling = mutationQueue.shift();
    if(isHandling){
        console.log('Re-entry!');
        //  Throttle/debounce and completely different handling logic
        return;
    }
    
    setTimeout(()=> {
        foo.setAttribute('class','bar');
        foo.setAttribute('class','');
        mutationQueue.push(true);
    }, 1000 * Math.random());
}

mutationObserver=new MutationObserver(handler);
mutationObserver.observe(foo, {
    attributes:true,
    characterData:true,
    childList:true,
    subtree:true
});

function randomIntervals() {
    setTimeout(()=>{
        foo.setAttribute('class','bar');
        foo.setAttribute('class','');
        mutationQueue.push(false);
        randomIntervals();
    }, 1000 * Math.random())
}

randomIntervals();
&lt;div id='foo'&gt;&lt;/div&gt;

您必须确保在代码中将触发您的异步处理程序的每个点添加适当的值,否则整个事情都会搞砸。我自己从来没有这样做过,我只是为这个问题想到了它,但似乎很容易做错。但是,我担心这可能是做你想做的事情的唯一方法。

【讨论】:

  • 嗯,在第一个 handler() 调用之后,你的 mutationQueue 将永远是 [true],因为第一个 false 被弹出并且没有其他值被推入它......
  • @Dinu 我刚刚复制了你的代码,我想你会在if (isHandling) 部分做一些事情来使它可用。如果没有,您可以将其设为 if...else 声明。
  • 我的意思是,isHandling 什么时候不是真的?使用您的代码,每秒钟调用一次handler,无论它是从哪里调用的。
  • @Dinu 这不是您想要的行为吗?您想要的是能够知道何时在 handler 函数中进行了更改,不是吗?在您的示例代码中,handler 每次从处理程序外部进行更改时都会进行自己的更改。
  • 另外,我错过了return; 声明,这就是我建议if...else 的原因,忘记我说过了。
猜你喜欢
  • 2014-10-19
  • 2021-11-07
  • 2022-01-17
  • 2019-06-22
  • 2018-05-15
  • 1970-01-01
  • 2021-08-27
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多