当动画结束时,React组件在改变状态时会闪烁。

3
我正在尝试制作一个带有以下方法的React幻灯片组件:
- 组件通过props接收一个幻灯片数组(React组件),一次显示一个幻灯片,从幻灯片编号1开始(索引0)。 - 当前显示的幻灯片(也称为“当前幻灯片”)会显示出来,而其他幻灯片则不会显示。 - 在导航时(开始幻灯片动画),目标幻灯片将被添加到包装器中。如果目标幻灯片在当前幻灯片之前(即目标幻灯片在幻灯片数组中具有较小的索引,即点击下一个),它将以从左侧滑入的动画效果添加,动画效果为`left`从`-100%`到`0`(从左侧滑入);如果目标幻灯片在当前幻灯片之后(即目标幻灯片具有较大的索引,即点击上一个),它将以从右侧滑入的动画效果添加,动画效果为`from 100% to 0`(从右侧滑入);对于当前幻灯片,将相应地添加一个动画类`left 0 to 100%`或`left 0 to -100%`(向右或向左滑出)。 - 当动画结束时,当前幻灯片将更改为滑入的幻灯片(目标幻灯片),并且以前的当前幻灯片将被移除。

Slides component (glitching):

import React, { useCallback, useState } from 'react';

import styles from './Slides.module.css';

function Slides({ slides, onBrowse, onBrowseEnd, className, ...rest }) {
  const [currentSlide, setCurrentSlide] = useState(1); //slide number

  const [animSlide, setAnimSlide] = useState(null); //destination slide number

  const browse = useCallback((slide) => {
    if (slide <= slides.length) {
      setAnimSlide(slide);
      console.log(slide);
    }
    onBrowse && onBrowse(slide);
  });

  console.log('rerender');

  const animationEndCallback = useCallback(() => {
    setCurrentSlide(animSlide);
    setAnimSlide(null);
  }, [animSlide]);

  return (
    <div className={styles.wrapper + ' ' + className} {...rest}>
      <div className={styles.slides}>

        {animSlide &&
          React.cloneElement(slides[animSlide - 1], {
            className: `
              ${styles.absolute} 
              ${styles.slide} 
              ${slides[animSlide - 1]?.props?.className} 
              ${
                animSlide < currentSlide
                  ? styles.slide_in_from_left
                  : styles.slide_in_from_right
              }
            `,
          })}

        {React.cloneElement(slides[currentSlide - 1], {
          className: `
            ${styles.absolute} 
            ${styles.slide} 
            ${slides[currentSlide - 1]?.props?.className} 
            ${
              animSlide &&
              (animSlide < currentSlide
                ? styles.slide_right
                : styles.slide_left)
            }
          `,
          onAnimationEnd: animationEndCallback,
        })}
      </div>
      {
        
      }

      <div className={styles.navcon}>
        <button
          onClick={() => {
            browse(currentSlide - 1);
          }}
          disabled={!(currentSlide > 1)}
        >
          Previuos
        </button>

        <button
          onClick={() => {
            browse(currentSlide + 1);
          }}
          disabled={
            !(animSlide
              ? animSlide < slides.length
              : currentSlide < slides.length)
          }
        >
          Next
        </button>
      </div>
    </div>
  );
}

export default Slides;

我注意到,如果我将目标幻灯片放在包装组件中的当前幻灯片之后,就不会有任何问题,但如果我将其放在之前,它会闪烁。
我想了解为什么会这样?
不闪烁(动画幻灯片在当前幻灯片之后): 演示:https://stackblitz.com/edit/vitejs-vite-wnedwd?file=src%2FSlides.jsx enter image description here 闪烁(动画幻灯片在当前幻灯片之前): 演示:https://stackblitz.com/edit/vitejs-vite-hy6rsp?file=src%2FSlides.jsx

enter image description here

2个回答

0

差异

当动画正在运行时(阶段A),两个元素都会显示在DOM中,但当动画不运行时(阶段N),只有一个元素会显示。
如果我们关注“阶段A”,特别是JSX中返回的第二个元素。
在第一个示例中,它是在“阶段N”中独自显示的元素,然而,在第二个示例中,它不是,而是另一个元素。

影响

如果我们将代码包裹在回调函数内部,并使用setTimeout来查看动画结束后屏幕上显示的内容以及DOM中存在的两个元素(阶段A)

const animationEndCallback = useCallback(() => {
 setTimeout(()=> {
   setCurrentSlide(animSlide);
   setAnimSlide(null);
 },3000)
}, [animSlide]);

我们注意到返回的第二个元素总是在动画结束后显示在屏幕上的那个。
回答:
这解释了发生的情况。在动画结束时,组件尚未重新渲染,DOM中同时存在两者,事实上,正是在回调函数内部代码运行的那一刻,我们才看到第二个返回的元素在组件重新渲染之前显示在屏幕上,并且只有“currentSlide-1”克隆元素存在。我相当确定这就是闪烁的原因,以及为什么它不会在第一个示例中发生。
为什么?
现在,由于我是CSS初学者,我试图简化代码,摒弃动画,只包含两个元素,以理解为什么只有第二个元素在两者同时存在时被显示出来。
const [currentSlide, setCurrentSlide] = useState(1); // red one
const [animSlide, setAnimSlide] = useState(0); // pink one

return (
  //...
    <div className={styles.slides}>
      {React.cloneElement(slides[animSlide], { 
        className: `
        ${slides[animSlide]?.props?.className}
        ${styles.absolute}
       `,
      })}
      {React.cloneElement(slides[currentSlide], { 
        className: `
        ${slides[currentSlide]?.props?.className}
        ${styles.absolute}
       `,
      })}
    </div>
  //...
)

如果你尝试以上的代码,你会发现只有第二个会显示出来,所以这与动画无关。然而,当你尝试为两个都加上/移除 ${styles.absolute} 时,你会注意到具有 className={styles.absolute}position: absolute 的那个被显示出来,而且当两者都同时使用时,运行时接收到它的最后一个将处于前台的位置,这也解释了为什么在 Hadi KAR answer 中通过 z-index 来修复这个问题。

absolute

元素从正常文档流中移除,在页面布局中不为该元素创建空间。该元素相对于其最近的已定位祖先元素(如果有)或初始包含块定位。其最终位置由 top、right、bottom 和 left 的值决定。 当 z-index 的值不是 auto 时,此值会创建一个新的层叠上下文。绝对定位盒子的外边距不会与其他外边距折叠。


当我在两个元素中都去掉${styles.absolute}时,现在当没有人采用position: absolute时,我期望能看到它们两个,但结果令人困惑,这次我们只看到第一个,让我查看它们包装器div的CSS类,即styles.slides,在那里,你指定了flex: 1以及overflow: hidden
如果我们简化为这样:
.slides {
  flex: 1;
}

这样做会使得孩子们按照输入顺序一个接一个地显示在屏幕上,每个都填满整个屏幕。然而,如果我们在类中添加overflow: hidden,第二个孩子将被隐藏,因为它超出了屏幕范围。
最后,我想感谢你发表这个问题,它让我学到了新的CSS知识。

-2
我尝试了你的代码片段,通过将离开的幻灯片的 z-index: -1,我可以隐藏闪烁。所以无论 animSlide 或者 currentSlide 在你的层级结构中的位置如何,你可以确保 currentSlide 会保持在后面。

Slides.module.css

.back {
  z-index: -1;
}

Slides.js

//...
{React.cloneElement(slides[currentSlide - 1], {
          className: `
            ${styles.absolute} 
            ${styles.slide} 
            ${slides[currentSlide - 1]?.props?.className} 
            ${
              animSlide &&
              (animSlide < currentSlide
                ? styles.slide_right
                : styles.slide_left)
            }
            ${
              styles.back // add this line
            }
          `,
          onAnimationEnd: animationEndCallback,
        })}
//...

1
我的问题是为什么会发生这种情况,而不是如何防止它,正如我所说,我已经成功解决了。 - ATP
1
我的问题是为什么会发生,而不是如何防止它,正如我所说的,我已经成功解决了。 - ATP
显然,如果两个组件具有相同的z-index,后一个组件将自动位于前一个组件之上。试一试 - Hadi KAR
显然,如果两个组件具有相同的z-index,后一个组件将自动位于前一个组件之上。试一试 - Hadi KAR

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