-2

在32:50 的演讲中,演讲者展示了以下代码

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

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

他解释了为什么click当事件直接由用户交互引起时它会阻止事件的默认操作,而不是当调度是编程的时,例如link.click(). 有没有一种简单的方法可以使它在后一种情况下与前一种情况相同?

编辑以详细说明动机

@Kaiido 在评论中询问为什么我(或者更确切地说是演讲者)在这里使用 Promise - 我假设他想建议直接将调用preventDefault. 如果我想使用 Promise.all 或其他组合器将它与其他 Promise 组合,则 Promise 很方便。

4

1 回答 1

-1

该视频完美地解释了发生的事情,但似乎我们无论如何都必须解释它......

在“原生”事件的情况下,浏览器将“排队一个任务”以触发一个事件,该事件将被分派到您的目标并依次调用所有侦听器,最后调用它们的 JS 回调。由于每次回调执行之间 JS 堆栈都是空的,因此会执行一个微任务检查点,并且在这些回调期间解决的 Promise 会执行其回调。
只有在调用了所有这些侦听器之后,如果尚未引发事件的取消标志,则将执行默认操作。

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>

但是,对于由 JS 触发的合成事件,该事件是同步调度的,并且 JS 堆栈永远不会为空(因为它至少包含最初触发该事件的作业)。
因此,当浏览器在调用我们的每个 JS 回调后必须执行“运行脚本后清理”算法时,这一次它不会执行微任务检查点。
相反,它将一直持续到调度带有取消标志的事件算法的第 12 步仍然关闭,并将执行默认操作。只有在此之后,它才会从触发该合成事件的任何内容中返回,并且只有在浏览器能够执行微任务检查点之后,该脚本才会被清理。

在下面的代码片段中,我将使用一个<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>


所以现在我们只说了杰克在他的演讲中所说的话。

让我们更加关注 OP 的情况,他们希望仍然能够将其事件处理程序作为 Promise 处理,并希望能够防止事件的默认行为。

这是不可能的。

要让一个对象将 Promise 作为 Promise 使用,它的回调必须在 microtask-checkpoint 中执行。正如我们在上面看到的,微任务检查点只会在默认行为执行后才会执行。所以这是一个死胡同。

可以做什么:

  • 防止在事件处理程序中而不是在 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>
    但这意味着您要么阻止所有默认行为,要么在偶数处理程序中移动一些逻辑,也许一开始就破坏了在那里拥有 Promise 的想法。
    不过,如果您只是想在此事件之后链接某些东西,这可能是一个可行的解决方案。

  • 不要使用真正的 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>
    但这不是 Promise,不能与 Promise 链接,也不能被 Promise 的任何方法使用。

现在,电话是你的。

于 2021-03-15T03:15:02.573 回答