概述
你了解 Transition 吗?你可能并不了解 Transition?下面本篇文章就来通过图文结合的方式带大家深入了解一下Transition,希望对大家有所帮助!
这篇文章我们深入学习 Transition
动画。没错,CSS3 Transition
动画。你可能会问,不是很简单吗,这什么好讲的?
确实,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 动画用起来几乎没有成本,一直以来也没有太深入学习,最近翻看源代码和 MDN 文档之后发现有些知识没有理解到位,于是乎有了这篇文章,希望对读者更深入了解 Transition 动画有所帮助。(学习视频分享:css视频教程)
为了尽量降低阅读理解成本,这篇文章会写得稍微啰嗦一点点,大部分示例都会配图 ——【多图预警开始!】
什么是 Transition?
简单的说就是过渡动画,通常修改 DOM 节点的样式都是立即更新在页面上的,例如修改宽高,修改透明度,修改背景色等等。
例如当鼠标移动至按钮上时,为了突出按钮的可交互,会在 hover 时修改它的样式,让用户注意到它。没有加 transition 过渡动画,给用户的感觉会很僵很生硬。
.button {
// ...
background-color: #00a8ff;
}
.button:hover {
background-color: #fbc531;
transform: scale(1.2);
}
登录后复制
加上 transition 一行代码之后,变化就会比较顺滑。
.button {
// ...
transition: 1s;
}
// ...
登录后复制
这个例子中我们修改了 background-color
和 transform
,结合 transition
属性,浏览器就会自动让属性值随着时间变化,从旧值逐步过渡到过渡新值,视觉上就是动画效果。
需要注意,并不是所有的属性变化都会有过渡效果
有些 CSS 属性只支持枚举值,非黑即白,不存在中间状态,例如
visibility: visible;
被修改成visibility: hidden;
不会有动画效果,因为不存在可见又不可见的中间状态。在浏览器上的表现是 duration 到了之后元素立即突变为 hidden。.button:hover { //... visibility: hidden; }
登录后复制- 有些属性虽然是可计算数值,但天生注定不能有过渡效果,例如
transition-delay
,transition-duration
都是立即生效,这里值得补一句由于transition-*
属性是即时生效,这行代码如果是 hover 时才加上,那么效果会是 hover 时有动画,移出时没有动画。 即使是可过渡的属性变化,也可能因为无法计算中间状态而失去过渡效果。例如
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 上去立马就生效了。- 如果某个属性值是连续可计算的数值,但是变化前后变成散列的枚举值,那么过渡也不会生效。例如从
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
,且 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 章节找到下面这段话:
翻译一下,当样式变更事件发生时,实现(浏览器)必须根据变更的属性执行过渡动画。但如果样式变更事件发生时或上一次样式变更事件期间,元素不在文档中,则不会为该元素启动过渡动画。
结合浏览器构建 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");
});
}
登录后复制
优化方案三:Force Reflow
在规范文档中,还留意到以下这句话:
意思是说,浏览器通常还会在两种情况下会产生样式变更事件,一是满足屏幕刷新频率(不就是 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.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 效果:
完整的示例代码在这里,注意:这只是个快速实现用于演示的示例,有非常多的问题没有考虑在内,仅可用于学习参考。
结语
原本以为非常基础简单的知识点,分分钟可以写完这篇文章。没想到中途查文档,看资料,制作演示 DEMO 还是花了不少时间。好在整理资料的过程中也理清了很多知识点。希望这篇文章对你熟悉 Transition 动画有所帮助 。
相关推荐:web前端入门视频
以上就是你了解 Transition 吗?一起来深入了解下Transition!的详细内容,更多请关注靠谱客其它相关文章!
最后
以上就是整齐鲜花为你收集整理的你了解 Transition 吗?一起来深入了解下Transition!的全部内容,希望文章能够帮你解决你了解 Transition 吗?一起来深入了解下Transition!所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复