• 首页 首页 icon
  • 工具库 工具库 icon
    • IP查询 IP查询 icon
  • 内容库 内容库 icon
    • 快讯库 快讯库 icon
    • 精品库 精品库 icon
    • 问答库 问答库 icon
  • 更多 更多 icon
    • 服务条款 服务条款 icon

你 Transition 吗来下Transition

武飞扬头像
PHP中文网
帮助28

确实,Transition 动画使用起来非常容易。只需要给元素加上 transition-delay, transition-duration, transition-property, transition-timing-function 属性就可以有过滤效果。更简单的用法是直接使用简写的 transition 属性:

transition: <property> <duration> <timing-function> <delay>;

// transition-delay 默认为 0
// transition-property 默认为 all
// transition-timing-function 默认为 ease
transition: 0.3s;

什么是 Transition?

简单的说就是过渡动画,通常修改 DOM 节点的样式都是立即更新在页面上的,例如修改宽高,修改透明度,修改背景色等等。

例如当鼠标移动至按钮上时,为了突出按钮的可交互,会在 hover 时修改它的样式,让用户注意到它。没有加 transition 过渡动画,给用户的感觉会很僵很生硬。

.button {
  // ...
  background-color: #00a8ff;
}

.button:hover {
  background-color: #fbc531;
  transform: scale(1.2);
}

学新通技术网

加上 transition 一行代码之后,变化就会比较顺滑。

.button {
  // ...
  transition: 1s;
}
// ...

这个例子中我们修改了 background-colortransform,结合 transition 属性,浏览器就会自动让属性值随着时间变化,从旧值逐步过渡到过渡新值,视觉上就是动画效果。

学新通技术网

需要注意,并不是所有的属性变化都会有过渡效果

  1. 有些 CSS 属性只支持枚举值,非黑即白,不存在中间状态,例如 visibility: visible; 被修改成 visibility: hidden; 不会有动画效果,因为不存在可见又不可见的中间状态。在浏览器上的表现是 duration 到了之后元素立即突变为 hidden。

    .button:hover {
      //...
      visibility: hidden;
    }

    学新通技术网

  2. 有些属性虽然是可计算数值,但天生注定不能有过渡效果,例如 transition-delaytransition-duration 都是立即生效,这里值得补一句由于 transition-* 属性是即时生效,这行代码如果是 hover 时才加上,那么效果会是 hover 时有动画,移出时没有动画。
  3. 即使是可过渡的属性变化,也可能因为无法计算中间状态而失去过渡效果。例如 box-shadow 属性虽然支持 transition 的动画的,但如果从 "outset" 切换到 inset,也是突变的。

    .button {
      // ...
      box-shadow: 0 0 0 1px rgb(0 0 0 / 15%);
      transition: 1s;
    }
    
    .button:hover {
      // ...
      box-shadow: inset 0 0 0 10px rgb(0 0 0 / 15%);
    }
    学新通技术网
    从表现上看,box-shadow 的变化是 hover 上去立马就生效了。
  4. 如果某个属性值是连续可计算的数值,但是变化前后变成散列的枚举值,那么过渡也不会生效。例如从 height: 100px => height: auto 是不会有动画的。

以上的内容回顾了 Transition 的基本用法,下面我们来看一个在实际开发场景中会遇到的问题。

为什么 Transition 动画没有生效?

场景题:假设我们现在接到一个自定义下拉选择器的动画需求,设计师给到的效果图如下:

学新通技术网

这是很常见的出现-消失动画,在很多组件库里面都会出现,点击触发器(按钮)时才在页面上渲染 Popup (下拉内容),并且 Popup 出现的同时需要有渐现和下滑的动画;展开之后再次点击按钮,Popup 需要渐隐和上滑。

平时使用的时候并没有过多注意它的实现,不妨现在让我们动手试验一下。

暂时忽略 popup 的内容,用了个 div 来占位模拟,HTML 结构很简单。

<div class="wrapper">
    <div id="button"></div>
    <div id="popup"></div>
</div>

在点击按钮的时候,让 popup 显示/隐藏,然后切换 popup.active 类名。

const btn = document.querySelector("#button");
const popup = document.querySelector("#popup");

if (!popup.classList.contains("active")) {
    popup.style.display = "block";
    popup.classList.add("active");
} else {
    popup.style.display = "none";
    popup.classList.remove("active");
}

编写 CSS 样式,在不 active 时透明度设置为 0,向上偏移,active 时则不偏移且透明度设置为 1。

#popup {
  display: none;
  opacity: 0;
  transform: translateY(-8px);
  transition: 1s;

  &.active {
    opacity: 1;
    transform: translateY(0%);
  }
}

完整代码 在这里,看起来代码没什么问题,点击按钮切换的时候,popup 应该会有动画过渡效果。然而实际运行效果:

学新通技术网

硬邦邦地完全没有过渡效果,这是为啥?明明已经设置了 transition,且 opacitytranslateY 都是可计算可过渡的数值,也产生了变化,浏览器为什么不认呢?

在查文档之前,我们先尝试使用万精油 setTimeout

方案一:setTimeout 万精油

修改 JS 代码:

btn.addEventListener("click", () => {
  if (!popup.classList.contains("active")) {
    popup.style.display = "block";
    setTimeout(() => {
      popup.classList.add("active");
    }, 0);
  } else {
    popup.classList.remove("active");
    setTimeout(() => {
      popup.style.display = "none";
    }, 600);
  }
});

可以看到添加了 setTimeout 之后,transition 动画就生效了。

学新通技术网

隐藏时的 setTimeout 600ms 对应 CSS 中设置的 transition: 0.6s,就是动画完成之后才将 display 设置为 none

主要困惑的点在于为什么显示的时候也需要加 setTimeout 呢?setTimeout 0 在这里起到的作用是什么?带着问题去翻看规范文档。

在规范文档的 Starting of transitions 章节找到下面这段话:

翻译一下,当样式变更事件发生时,实现(浏览器)必须根据变更的属性执行过渡动画。但如果样式变更事件发生时或上一次样式变更事件期间,元素不在文档中,则不会为该元素启动过渡动画。

结合浏览器构建 RenderTree 的过程,我们可以很清晰地定位到问题:当样式变更时间发生时,display: none 的 DOM 元素并不会出现在 RenderTree 中(style.display='block' 不是同步生效的,要在下一次渲染的时候才会更新到 Render Tree),不满足 Starting of transitions 的条件。

学新通技术网

所以 setTimeout 0 的作用是唤起一次 MacroTask,等到 EventLoop 执行回调函数时,浏览器已经完成了一次渲染,再加上 .active 类名,就有了执行过渡动画的充分条件。

优化方案二:精准卡位 requestAnimationFrame

既然目的为了让元素先出现到 RenderTree 中,和渲染相关,很容易想到可以将 setTimeout 替换成 requestAnimationFrame,这样会更精准,因为 requestAnimation 执行时机和渲染有关。

if (!popup.classList.contains("active")) {
    popup.style.display = "block";

    requestAnimationFrame(() => {
        popup.classList.add("active");
    });
}

补充一个小插曲:在查找资料的过程中了解到 requestAnimationFrame 的规范是要求其回调函数在 Style/Layout 等阶段之前执行,起初 Chrome 和 Firefox 是遵循规范来实现的。而 Safari 和 Edge 是在执行的时机是在之后。 从现在的表现上来看,Chrome 和 Firefox 也改成了在之后执行,翻看以前的文档会说需要嵌套两层 requestAnimationFrame,现在已经不需要了。Is requestAnimationFrame called at the right point?

优化方案三:Force Reflow

在规范文档中,还留意到以下这句话:

Implementations typically have a style change event to correspond with their desired screen refresh rate, and when up-to-date computed style or layout information is needed for a script API that depends on it.

意思是说,浏览器通常还会在两种情况下会产生样式变更事件,一是满足屏幕刷新频率(不就是 requestAnimationFrame?),二是当 JS 脚本需要获取最新的样式布局信息时。

在 JS 代码中,有些 API 被调用时,浏览器会同步地计算样式和布局,频繁调用这些 API(offset*/client*/getBoundingClientRect/scroll*/...等等)通常会成为性能瓶颈。

学新通技术网

然而在这个场景却可以产生奇妙的化学反应:

if (!popup.classList.contains("active")) {
  popup.style.display = "block";
  popup.scrollWidth;
  popup.classList.add("active");
}

注意看,我们只是 display 和 add class 之间读取了一下 scrollWidth,甚至没有赋值,过渡动画就活过来了。

学新通技术网

原因是 scrollWidth 强制同步触发了重排重绘,再下一行代码时,popup 的 display 属性已经更新到 Render Tree 上了。

优化方案四:过渡完了告诉我 onTransitionEnd

现在【出现】动画已经搞明白了,在看开源库的源码中发现像 vue, bootstrap, react-transition-group 等库都是使用了 force reflow 的方法,而 antd 所使用的 css-animte 库则是通过设置 setTimeout。

【消失】动画还不够优雅,前面我们是直接写死 setTimeout 600,让元素在动画结束时消失的。这样编码可复用性差,修改动画时间还得改两处地方(JS CSS),有没有更优雅的实现?

popup.classList.remove("active");setTimeout(() => {
    popup.style.display = "none";
}, 600);

文档中也提到了 Transition Events,包括 transitionruntransitionstarttransitionendtransitioncancel,看名字就知道事件代表什么意思,这里可以用 transitionend 进行代码优化。

if (!popup.classList.contains("active")) {
    popup.style.display = "block";
    popup.scrollWidth;
    popup.classList.add("active");
} else {
    popup.classList.remove("active");
    popup.addEventListener('transitionend', () => {
        popup.style.display = "none";
    }, { once: true })
}

需要注意 transition events 同样也有冒泡、捕获的特性,如果有嵌套 transition 时需要留意 event.target

到这里我们已经用原生 JS 完成了一个出现、消失的动画实现,完整的代码在这里。文章的最后,我们参照 vue-transition 来开发一个 React Transition 的单个元素动画过渡的最小实现。

仿 v-transition 实现一个 React Transition 组件

学新通技术网

根据动画过程拆分成几个过程:

  • enter 阶段渲染 DOM 节点,初始化动画初始状态(添加 *-enter 类名)
  • enter-active 阶段执行 transition 过渡动画(添加 *-enter-active 类名)
  • enter-active 过渡完成之后进入正常展示阶段(移除 *-enter-active 类名)

enter-to 和 leave-to 暂时用不上,leave 阶段和 enter 基本一致也不再赘述。

直接看代码:

export const CSSTransition = (props: Props) => {
  const { children, name, active } = props;
  const nodeRef = useRef<HTMLElement | null>(null);
  const [renderDOM, setRenderDOM] = useState(active);

  useEffect(() => {
    requestAnimationFrame(() => {
      if (active) {
        setRenderDOM(true);
        nodeRef.current?.classList.add(`${name}-enter`);
        // eslint-disable-next-line @typescript-eslint/no-unused-expressions
        nodeRef.current?.scrollWidth;
        nodeRef.current?.classList.remove(`${name}-enter`);
        nodeRef.current?.classList.add(`${name}-enter-active`);

        nodeRef.current?.addEventListener("transitionend", (event) => {
          if (event.target === nodeRef.current) {
            nodeRef.current?.classList.remove(`${name}-enter-active`);
          }
        });
      } else {
        nodeRef.current?.classList.add(`${name}-leave`);
        // eslint-disable-next-line @typescript-eslint/no-unused-expressions
        nodeRef.current?.scrollWidth;
        nodeRef.current?.classList.remove(`${name}-leave`);
        nodeRef.current?.classList.add(`${name}-leave-active`);

        nodeRef.current?.addEventListener("transitionend", (event) => {
          if (event.target === nodeRef.current) {
            nodeRef.current?.classList.remove(`${name}-leave-active`);
            setRenderDOM(false);
          }
        });
      }
    });
  }, [active, name]);

  if (!renderDOM) {
    return null;
  }

  return cloneElement(Children.only(children), {
    ref: nodeRef
  });
};

这个组件接收三个 props,分别是

  • children 需要做过渡动画的 ReactElement,只允许传一个 Element
  • name 过渡动画的 css 类名前缀
  • active 布尔值,用于区分是进场还是消失

使用方式:

<CSSTransition name="fade" active={active}>
    // 一个需要做过渡动画的 ReactElement
</CssTransition>

借助 transition-delay,加一点技巧实现 stagger 效果:

学新通技术网

完整的示例代码在这里,注意:这只是个快速实现用于演示的示例,有非常多的问题没有考虑在内,仅可用于学习参考。

结语

这篇好文章是转载于:学新通技术网

  • 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
  • 本站站名: 学新通技术网
  • 本文地址: /boutique/detail/tanfgffk
系列文章
更多 icon
同类精品
更多 icon
继续加载