好得很程序员自学网

<tfoot draggable='sEl'></tfoot>

你了解 Transition 吗?一起来深入了解下Transition!

你 了解 Trans IT ion 吗?你可能并不了解 Transition?下面本篇 文章 就来通过图文结合的方式带大家深入了解一下Transition, 希望对大家有所帮助 !

这篇文章我们深入学习 Transition 动画。没错, CSS3 Transition 动画。你可能会问,不是很 简单 吗,这什么好讲的?

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

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

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

由于 transition 动画用起来几乎没有 成本 ,一直以来也没有太深入学习,最近翻看 源 代码和 MDN 文档之后发现有些知识没有理解到位,于是乎有了这篇文章,希望对读者更深入了解 Transition 动画有所帮助。(学习视频分享:css视频教程)

为了尽量降低阅读理解成本,这篇文章会写得稍微啰嗦 一点 点,大部分示例都会配图 ——【多图预警 开始 !】

什么是 Transition?

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

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

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

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

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

@H_ 304 _49@.button { // ... transition: 1s; } // ...

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

区分于 Animation , Transition 动画侧重于表现 一次过渡效果,从开始到结束的变化 。而 Animation 不需要变化,可以循环播放 ▶️。

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

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

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


有些属性虽然是可计算数值,但天生注定不能有过渡效果,例如 transition-delay , transition-duration 都是立即 生效 ,这里值得补一句由于 transition-* 属性是即时生效,这行代码如果是 hover 时才加上,那么效果会是 hover 时有动画,移出时没有动画。

即使是可过渡的属性变化,也可能因为无法计算中间状态而失去过渡效果。例如 box -s hadow 属性虽然支持 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 上去立马就生效了。 如果某个属性值是连续可计算的数值,但是变化前后变成散列的枚举值,那么过渡也不会生效。例如从 h ei ght: 100px => height: auto 是不会有动画的。

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

为什么 Transition 动画没有生效?

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

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

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

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

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

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

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

if (!popup.classList.cont ai ns("active")) {
    popup.style.dis play  = "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;

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

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

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

在查文档之前,我们先尝试使用万精油 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 章节找到下面这段话:

When a style change event occurs, implementations must start transitions based on the computed values t hat changed in that event. If an element is not in the document during that style change event or was not in the document during the p rev ious style change event, then transitions are not started for that element in that style change event.

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

结合浏览器构建 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 等 阶段 之前执行,起初 Ch rom e 和 Firefox 是遵循规范来实现的。而 Safari 和 Edge 是在执行的时机是在之后。 从现在的表现上来看,C hr ome 和 Firefox 也改成了在之后执行,翻看以前的文档会说需要嵌套两层 requestAnimationFrame,现在已经不需要了。Is requestAnimationFrame called at the right point?

优化方案三:Force Reflow

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

Implementations typ ical ly have a style change event to correspond with their desi red 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,包括 transitionrun , transitionstart , transitionend , transitioncancel ,看名字就 知道 事件代表什么意思,这里可以用 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.t arg et 。

到这里我们已经用原生 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] = use stat e(active);

  useEffect(() => {
    requestAnimationFrame(() => {
      if (active) {
        setRenderDOM(true);
        nodeRef. current ?.classList.add(`${name}-enter`);
        // eslint-disable-next-line @typescript-eslint/no-unused-ex Press ions
        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-e xp ressions
        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 效果:

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

结语

原本以为非常基础简单的知识点,分分钟可以写完这篇文章。没想到中途查文档,看资料, 制作 演示 DEMO 还是花了不少时间。好在整理资料的过程中也理清了很多知识点。希望这篇文章对你熟悉 Transition 动画有所帮助 。

相关推荐:web前端入门视频

以上就是你了解 Transition 吗?一起来深入了解下Transition!的详细内容,更多请关注其它相关文章!

总结

以上是 为你收集整理的 你了解 Transition 吗?一起来深入了解下Transition! 全部内容,希望文章能够帮你解决 你了解 Transition 吗?一起来深入了解下Transition! 所遇到的问题。

如果觉得 网站内容还不错, 推荐好友。

查看更多关于你了解 Transition 吗?一起来深入了解下Transition!的详细内容...

  阅读:18次