在无限滑块轨道动画上使用 Element.prepend 的意外 DOM 排序行为

Posted

技术标签:

【中文标题】在无限滑块轨道动画上使用 Element.prepend 的意外 DOM 排序行为【英文标题】:Unexpected DOM Ordering Behaviour With Element.prepend on Infinite Slider-Track Animation 【发布时间】:2021-12-26 21:18:08 【问题描述】:

我正在研究一个无限滑块,但我遇到了一个奇怪的 DOM 排序错误。

在每次动画迭代结束时,最后一个子元素应该添加到 div.slider-track 元素之前,因此它会在滑块的开头呈现并在下一次迭代之前将其他卡片向上推。在 第 11 次迭代 之前,它会按预期工作,其中 Card1 被预先添加连续两次迭代。 Card1 被选为 lastChild 属性两次。不应该在第二个第 11 次迭代中选择 Card10 吗?什么给了?

const sliderTrack = document.querySelector(".slider-track")

const newCard = (count) => 
  const card = document.createElement("div");
  card.className = "card";
  
  const label = document.createElement("span")
  label.innerText = `Card $count`
  label.className = "label";
  card.append(label)
  
  return card


const populateCards = (element) => 
  for (let i = 1; i <= 10; i++) 
  element.append(newCard(i))
  


sliderTrack.addEventListener('animationiteration', () => 
  sliderTrack.prepend(sliderTrack.lastChild);
);

populateCards(sliderTrack)
body 
  background: #f06d06;
  font-family: 'Roboto', sans-serif;
  font-weight: bold;
  padding: 0;


.slider 
  overflow: hidden;


@keyframes slider 
  to 
    transform: translate(10%);
  


.slider-track 
  display: flex;
  animation: slider 1s linear;
  animation-iteration-count: infinite;


.card 
  background: white;
  width: 10vw;
  height: 10vw;
  border-radius: 8px;
  box-shadow: 2px 2px rgba(0, 0, 0, 20%);
  
  display: flex;
  justify-content: center;
  align-items: center;
<div class="slider">
  <div class="slider-track">
  </div>
</div>

【问题讨论】:

【参考方案1】:

您的问题是由 DOM 的一个稍微微妙且经常令人讨厌的方面引起的。这就是“文本节点”——即作为 html 一部分的任何纯文本——也算作节点,因此也算作其父元素的子元素。

在这种情况下,因为滑块轨道的 HTML 是跨 2 行编写的:

<div class="slider-track">
</div>

那么,不管你信不信,你实际上有一个换行符,字符串"\n" - 作为滑块轨道元素的子音符之一。

因此,当您在初始化时将卡片 1 填充到卡片 10 时,您实际上有 11 个子节点,首先是该文本节点。在您的动画迭代 10 次后,它以该文本节点作为最后一个元素结束。

此时,sliderTrack.prepend(sliderTrack.lastChild) 只是将那个无辜的换行文本节点从滑块轨道的 DOM 子节点的末尾移动到前面。这没有明显的效果 - 但因为这是在需要 1 秒的动画之后发生的,这意味着在那个特定的“滴答声”上,似乎没有发生任何事情,这是您正在观察的奇怪和不受欢迎的行为。

幸运的是,当您意识到这是问题所在时,解决方法很简单。您当然可以通过将结束标记与开始标记放在同一行来简单地删除换行符文本,甚至没有任何空格:

const sliderTrack = document.querySelector(".slider-track")

const newCard = (count) => 
  const card = document.createElement("div");
  card.className = "card";
  
  const label = document.createElement("span")
  label.innerText = `Card $count`
  label.className = "label";
  card.append(label)
  
  return card


const populateCards = (element) => 
  for (let i = 1; i <= 10; i++) 
  element.append(newCard(i))
  


sliderTrack.addEventListener('animationiteration', () => 
  sliderTrack.prepend(sliderTrack.lastChild);
);

populateCards(sliderTrack)
body 
  background: #f06d06;
  font-family: 'Roboto', sans-serif;
  font-weight: bold;
  padding: 0;


.slider 
  overflow: hidden;


@keyframes slider 
  to 
    transform: translate(10%);
  


.slider-track 
  display: flex;
  animation: slider 1s linear;
  animation-iteration-count: infinite;


.card 
  background: white;
  width: 10vw;
  height: 10vw;
  border-radius: 8px;
  box-shadow: 2px 2px rgba(0, 0, 0, 20%);
  
  display: flex;
  justify-content: center;
  align-items: center;
<div class="slider">
  <div class="slider-track"></div>
</div>

虽然这在这里并不算太痛苦,但在其他情况下肯定会很烦人,迫使您以难以阅读的方式格式化 HTML。

所以对于这种情况,还有另一种解决方案——除了lastChild,还有更具体的lastElementChild,它专门忽略文本节点。这也可以在这里完美地工作,并且通常可能是更好的解决方案:

const sliderTrack = document.querySelector(".slider-track")

const newCard = (count) => 
  const card = document.createElement("div");
  card.className = "card";
  
  const label = document.createElement("span")
  label.innerText = `Card $count`
  label.className = "label";
  card.append(label)
  
  return card


const populateCards = (element) => 
  for (let i = 1; i <= 10; i++) 
  element.append(newCard(i))
  


sliderTrack.addEventListener('animationiteration', () => 
  sliderTrack.prepend(sliderTrack.lastElementChild);
);

populateCards(sliderTrack)
body 
  background: #f06d06;
  font-family: 'Roboto', sans-serif;
  font-weight: bold;
  padding: 0;


.slider 
  overflow: hidden;


@keyframes slider 
  to 
    transform: translate(10%);
  


.slider-track 
  display: flex;
  animation: slider 1s linear;
  animation-iteration-count: infinite;


.card 
  background: white;
  width: 10vw;
  height: 10vw;
  border-radius: 8px;
  box-shadow: 2px 2px rgba(0, 0, 0, 20%);
  
  display: flex;
  justify-content: center;
  align-items: center;
<div class="slider">
  <div class="slider-track">
  </div>
</div>

【讨论】:

非常感谢!是的,第二种解决方案更有意义,因为无论如何我都想选择元素。好奇怪的细节,谢谢你的解释,如果你不解释,我以后肯定会再次遇到它。

以上是关于在无限滑块轨道动画上使用 Element.prepend 的意外 DOM 排序行为的主要内容,如果未能解决你的问题,请参考以下文章

图像不会在轨道滑块基础上滑动

使用 CSS 的无限图标滑块

在 UIslider 轨道上获取事件

单击滑块轨道时获取 Jquery-UI 滑块值?

自定义滑块轨道图像

当滑块按钮超过最大值的 60% 时,UISlider 最大轨道图像使用最小轨道图像