为什么我需要调用两次requestAnimationFrame来等待DOM渲染完成

4

我试图找到鼠标指针下的新渲染元素。 (*)

这是我的代码:

btn.addEventListener('click', function () {
  btn.remove();
  for (let i = 0; i < 10; i++) {
    lst.appendChild(document.createElement('li')).textContent = 'Element ' + i;
  }
  requestAnimationFrame(function () { requestAnimationFrame(function () {
    const chosen = document.querySelector('li:hover');
    alert(chosen && 'Your mouse on ' + chosen.textContent); // do something more with chosen
  }); });
});
#btn { width: 200px; height: 200px; }
#lst { width: 200px; line-height: 20px; display: block; padding: 0; }
#lst li { display: block; height: 20px; width: 200px; overflow: hidden; }
#lst li:hover { background: #ccc; }
<button id=btn>Click Me</button>
<ul id=lst><ul>

我很困惑,需要使用两个requestAnimationFrame才能使我的代码正确执行。删除其中一个raf,警报将显示null

这段代码对我来说似乎很丑陋。如何更优雅地实现它?


如果有人关心:我在Firefox上运行我的代码。而且,作为我的Firefox扩展的一部分,该代码只需针对Firefox 60+进行目标定位。

(*): 故事背后可能更加复杂。但为了简单起见...


有趣...我想指出的是,移动鼠标实际上会更新:hover,当我用鼠标点击时,它确实移动了。所以对我来说更好的测试用例是将所有内容都包装在一个超时中,这样我就可以停止触摸鼠标:https://jsfiddle.net/1ky05m46/ 如果我们将.remove()更改为设置display:none规则,那么我们可以看到按钮仍然是具有:hover的按钮... https://jsfiddle.net/1ky05m46/1/ 我想这是不可能的...请注意,在这种情况下,Chrome甚至直到鼠标移动才会更新它... - Kaiido
2个回答

2

您发现的这种行为相当有趣,浏览器似乎在第二帧之前不更新:hover,即使我们强制重新布局或其他操作。

更糟糕的是,在Chrome中,如果使用display:none隐藏<button>元素,则它将保持:hover元素状态,直到鼠标移动(而通常情况下display:none元素不可访问:hover)。

规范没有详细说明如何计算:hover,因此很难说这是一个“错误”。

无论如何,对于您想要的内容,最好的方法是通过document.elementsFromPoints方法找到该元素,该方法将同步工作。

btn.addEventListener('click', function ( evt ) {
  btn.remove();
  for (let i = 0; i < 10; i++) {
    lst.appendChild(document.createElement('li')).textContent = 'Element ' + i;
  }
  const chosen = document.elementsFromPoint( evt.clientX, evt.clientY )
    .filter( (elem) => elem.matches( "li" ) )[ 0 ];

  alert(chosen && 'Your mouse on ' + chosen.textContent); // do something more with chosen
});
#btn { width: 200px; height: 200px; }
#lst { width: 200px; line-height: 20px; display: block; padding: 0; }
#lst li { display: block; height: 20px; width: 200px; overflow: hidden; }
#lst li:hover { background: #ccc; }
<button id=btn>Click Me</button>
<ul id=lst><ul>


1
我正在尝试这种方法。由于我的DOM更新逻辑不在鼠标事件中,因此我必须在每个mousemove事件上存储位置。无论如何,它至少可以工作。而且...异步操作可能会导致更多的错误行为,我想避免它们。 - tsh
我在一个更通用的情况下遇到了这种行为 - 我试图在执行一些耗时工作之前显示一个繁忙指示器,但是除非我在更新指示器之后两次请求动画帧,否则它直到工作完成后才会显示(这当然使其无用)。 - Michael
@Michael 这是另一个问题,只是有些松散地相关。请看一下我关于那个“问题”的回答:https://stackoverflow.com/a/63929232/3702797 - Kaiido
谢谢!在我的情况下,我找到了原因是我在异步函数中使用了await。这实际上使得函数的剩余部分(或直到另一个await)在"回调"返回之前执行。加上第二个await会导致类似立即"返回"的效果,就像我在使用回调函数一样,并且允许窗口重新绘制。 - Michael

0

我无法确切地回答你为什么需要两个 rafs 的问题。

但是我可以用 async / await 提供更优雅的方式。创建一个名为 nextTick 的小函数,它返回一个 promise。这样你就可以等待下一帧。

所以你可以先等待按钮消失,创建你的元素,然后再次等待下一个绘画周期,以确保元素可访问。

btn.addEventListener('click', async function () {
  btn.remove();
  await nextTick();
  for (let i = 0; i < 10; i++) {
    lst.appendChild(document.createElement('li')).textContent = 'Element ' + i;
  }
  await nextTick()
  const chosen = document.querySelector('li:hover');
  alert(chosen && 'Your mouse on ' + chosen.textContent); // do something more with chosen
});

function nextTick() {
   return new Promise(requestAnimationFrame)
}
#btn { width: 200px; height: 200px; }
#lst { width: 200px; line-height: 20px; display: block; padding: 0; }
#lst li { display: block; height: 20px; width: 200px; overflow: hidden; }
#lst li:hover { background: #ccc; }
<button id=btn>Click Me</button>
<ul id=lst><ul>


2
你如何确保在两个浏览器版本中这种行为不会改变?使用双重rAF是丑陋的,因为没有定义为什么它起作用的逻辑,而不是因为嵌套回调本身就很丑陋。例如,正如我在评论中指出的那样,如果OP使用display:none隐藏按钮,则在Chrome中需要新的鼠标移动,而2个rAFs是不够的。 - Kaiido

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接