简体   繁体   English

使用以编程方式分派的事件防止默认操作

[英]Prevent default action with a programatically dispatched event

In this talk at 32:50 the speaker shows this code:在 32:50 的演讲中,演讲者展示了以下代码:

const nextClick = new Promise(resolve => {
    link.addEventListener('click', resolve, { once: true });
});

nextClick.then(event => {
    event.preventDefault();
    // Handle event
});

He explains why it prevents the click event's default action when it's caused directly by user interaction, but not when the dispatch is programmatic, eg link.click() .他解释了为什么当点击事件直接由用户交互引起时,它会阻止click事件的默认操作,而不是在调度是程序化的情况下,例如link.click() Is there a simple way to make it work in the latter case the same as in the former?有没有一种简单的方法可以使它在后一种情况下与前一种情况相同?

Edit to elaborate on motivation编辑以详细说明动机

@Kaiido asked in the comments why I (or rather the speaker) use a Promise here – I assume he'd like to suggest attaching directly as an event listener the function which calls preventDefault . @Kaiido 在评论中询问为什么我(或者更确切地说是演讲者)在这里使用 Promise - 我假设他想建议将 function 作为事件侦听器直接附加到调用preventDefault The promise is convenient if I want to combine it with other promises using Promise.all or some other combinator.如果我想使用 Promise.all 或其他组合器将 promise 与其他承诺结合使用,则 promise 很方便。

The video explains perfectly what happens, but it seems we have to paraphrase it anyway...该视频完美地解释了发生的事情,但似乎我们无论如何都必须解释它......

In the case of a "native" event, the browser will "queue a task" to fire an event , which will get dispatched to your target and invoke all the listeners one after another, and finally call their JS callback .在“原生”事件的情况下,浏览器将“排队一个任务”以触发一个事件,该事件将被分派到您的目标并依次调用所有侦听器, 最后调用它们的 JS 回调 Since the JS stack is empty between each callback execution, a microtask-checkpoint is performed , and the Promises that got resolved during these callbacks get their callbacks executed.由于每次回调执行之间 JS 堆栈都是空的,因此会执行一个微任务检查点,并且在这些回调期间解决的 Promise 会执行其回调。
Only after all these listeners have been invoked, the default action will get executed, if the event's cancelled flag hasn't been raised.只有在调用了所有这些侦听器之后,如果尚未引发事件的取消标志,则将执行默认操作。

 const link = document.querySelector("a"); link.addEventListener("click", (evt) => { console.log("event 1:", evt.defaultPrevented); Promise.resolve().then( () => { console.log("microtask 1:", evt.defaultPrevented); // false evt.preventDefault(); }); }); link.addEventListener("click", (evt) => { console.log("event 2:", evt.defaultPrevented); // true Promise.resolve().then( () => { console.log("microtask 2:", evt.defaultPrevented); // true }); });
 #target { margin-top: 600vh; }
 <a href="#target">go to target</a> <div id="target">target</div>

However, in the case of a synthetic event fired by JS, that event is dispatched synchronously and the JS stack is never empty (since it at least contains the job that did fire the event in the first place).但是,对于由 JS 触发的合成事件,该事件是同步调度的,并且 JS 堆栈永远不会为空(因为它至少包含最初触发该事件的作业)。
So when the browser has to perform the " clean up after running script " algorithm after it called each of our JS callbacks, it will not perform a microtask-checkpoint this time.因此,当浏览器在调用我们的每个 JS 回调后必须执行“ 运行脚本后清理”算法时,这一次它不会执行微任务检查点。
Instead, it will continue until the step 12 of the dispatch an event algorithm with the cancelled flag still down, and will execute the default action.相反,它将一直持续到调度事件算法的第 12 步,并且取消标志仍然处于关闭状态,并将执行默认操作。 Only after this, it will return from whatever did fire that synthetic event, and only after that script will get cleaned after the browser will be able to execute the microtask-checkpoint.只有在此之后,它才会从触发该合成事件的任何内容中返回,并且只有在浏览器能够执行微任务检查点之后,该脚本才会被清理。

In the following snippet I'll use an <input type="checkbox"> element because its activation behavior is synchronous, whereas <a> 's "navigate a link" isn't and thus doesn't make for a good example.在下面的代码片段中,我将使用<input type="checkbox">元素,因为它的激活行为是同步的,而<a>的“导航链接”不是,因此不是一个很好的例子。

 const input = document.querySelector("input"); input.addEventListener("click", (evt) => { console.log("event fired"); Promise.resolve().then( () => { console.log("microtask fired, preventing"); evt.preventDefault(); }); }); console.log("before click, is checkbox checked:", input.checked); input.click(); console.log("after click, is checkbox checked:", input.checked);
 #target { margin-top: 600vh; }
 <input type="checkbox"> <div id="target">target</div>


So now we just said what Jake said in his presentation.所以现在我们只说了杰克在他的演讲中所说的话。

Let's make it a bit more focused on OP's case who want to still be able to deal with their event handler as a Promise, and want to be able to prevent the default behavior of the event.让我们更加关注 OP 的情况,他们希望仍然能够将其事件处理程序作为 Promise 处理,并希望能够防止事件的默认行为。

This is not possible.这是不可能的。

For an object to work with Promises as a Promise its callback has to get executed in a microtask-checkpoint.为了让 object 与 Promises 一起作为 Promise 工作,它的回调必须在微任务检查点中执行。 As we've seen above, the microtask-checkpoint will only get performed after the default behavior has been executed.正如我们在上面看到的,微任务检查点只会在默认行为执行才会执行。 So this is a dead-end.所以这是一个死胡同。

What could be done:可以做什么:

  • prevent the default behavior in the event handler rather than in the Promise reaction:阻止事件处理程序中的默认行为,而不是 Promise 反应中的默认行为:

     const link = document.querySelector("a"); const prom = new Promise( (resolve) => { link.addEventListener("click", (evt) => { evt.preventDefault(); resolve(evt); }, { once: true } ); }); prom.then( (evt) => console.log("clicked") ); // true link.click();
     #target { margin-top: 600vh; }
     <a href="#target">go to target</a> <div id="target">target</div>
    But this means that either you prevent all default behaviors, either you move some logic inside the even handler, maybe defeating the idea of having a Promise there in the first place.但这意味着您要么阻止所有默认行为,要么在偶数处理程序中移动一些逻辑,这可能会破坏首先在那里拥有 Promise 的想法。
    Still, if you just want something to be chained after this event, this might be a viable solution.不过,如果您只是想在此事件之后链接某些东西,这可能是一个可行的解决方案。

  • Don't use a real Promise but just something with a similar API, that will execute the callback synchronously:不要使用真正的 Promise,而只是使用类似 API 的东西,它将同步执行回调:

     class EventReaction { constructor( executor ) { this._callbacks = []; executor( this._resolver.bind( this ) ); } after( callback ) { if( this._done ) { callback(); } else { this._callbacks.push( callback ); } } _resolver( arg ) { this._callbacks.forEach( cb => cb( arg ) ); this._callbacks.length = 0; } } const link = document.querySelector("a"); const event_reaction = new EventReaction( (resolve) => { link.addEventListener("click", (evt) => { resolve(evt); }, { once: true } ); } ); event_reaction.after( (evt) => { console.log("preventing"); evt.preventDefault(); }); link.click();
     #target { margin-top: 600vh; }
     <a href="#target">go to target</a> <div id="target">target</div>
    But this is not a Promise, and can't be chained with a Promise nor used by any of the Promise's methods.但这不是 Promise,不能与 Promise 链接,也不能被 Promise 的任何方法使用。

Now, the call is yours.现在,电话是你的。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM