在几年前 jQuery 流行的时候你们都经过js去操做dom元素的css来实现以及监听动画,甚至出现了不少经过js去监听动画的动画库。
前端时间在写 vue 的时候发现 vue 中实现动画效果,并无经过 js 去不停的操做css样式,那么在css中是怎么去监听dom元素的动画效果呢?javascript
实现下图中的动画效果监听:css
#demo { width: 200px; height: 200px; background: red; opacity: 1; margin-bottom: 20px; transition: opacity 1s; } #demo.hide { opacity: 0; } #demo.show { opacity: 1; }
<div id="demo">opacity</div> <button onclick="runAction();">togglether</button>
(function() { var $target = document.getElementById('demo'); var transitions = { 'transition': 'transitionend', 'OTransition': 'oTransitionEnd', 'MozTransition': 'transitionend', 'WebkitTransition': 'webkitTransitionEnd' } var eventName = undefined; for(t in transitions){ if( $target.style[t] !== undefined ){ eventName = transitions[t]; break; } } eventName && $target.addEventListener(eventName, function() { alert('Transition end!'); }); runAction = function() { if (eventName) { var className = $target.className; $target.className = className.indexOf('hide') == -1 ? 'hide' : 'show'; } else { console.warn('您的浏览器不支持transitionend事件'); } } })();
代码很简单,就是经过js中的 transitionend 来监听动画执行效果,若是是帧动画的话,须要使用 animationend。
万变不离其宗,vue中实现动画监听也是基于 transitionend 来进行操做的。
效果传送门:https://codepen.io/pyrinelaw/pen/pqRgOehtml
公共样式长这样前端
.demo { height: 120px; position: relative; div { position: absolute; background: red; width: 100px; height: 100px; left: 0; top: 0; } }
<div class="demo demo-1"> <div v-bind:class="{anim: needAnim}" @transitionend="actionEnd"></div> </div>
export default { data() { return { needAnim: false, }; }, mounted() { setTimeout(() => { this.needAnim = true; }, 0); }, methods: { actionEnd() { alert('demo-1 action end'); }, }, };
一样的道理,帧动画须要使用 animationend, 后面再也不说明。
咱们来看一下vue中是如何作到的(代码太多,部分代码用“...”省略)。
关键代码: src/core/instance/state.jsvue
function initMethods (vm: Component, methods: Object) { for (const key in methods) { // 将事件绑定到虚拟Dom上 vm[key] = methods[key] == null ? noop : bind(methods[key], vm) // ... } }
与click事件的绑定无异,初始化的时候就把“transitionend”绑定到“VDom”上,以达到动画监听效果。java
有两种用法,一种是经过css控制动画效果node
.demo-3 { div { top: 20px; } /* 定义进入过渡的开始状态。在元素被插入以前生效,在元素被插入以后的下一帧移除 */ .anim-enter { left: 0px; } /* 定义进入过渡生效时的状态。在整个进入过渡的阶段中应用 */ /* 在元素被插入以前生效,在过渡/动画完成以后移除。这个类能够被用来定义进入过渡的过程时间,延迟和曲线函数 */ .anim-enter-active { transition: left 2s; } /* 定义进入过渡的结束状态。在元素被插入以后下一帧生效 (与此同时 v-enter 被移除),在过渡/动画完成以后移除 */ .anim-enter-to { left: 200px; } /* 定义离开过渡的开始状态。在离开过渡被触发时马上生效,下一帧被移除 */ .anim-leave { left: 200px; } /* 定义离开过渡生效时的状态。在整个离开过渡的阶段中应用 */ /* 在离开过渡被触发时马上生效,在过渡/动画完成以后移除。这个类能够被用来定义离开过渡的过程时间,延迟和曲线函数 */ .anim-leave-active { transition: left 2s; } /* 定义离开过渡的结束状态。在离开过渡被触发以后下一帧生效 (与此同时 v-leave 被删除),在过渡/动画完成以后移除 */ .anim-leave-to { left: 0px; } }
<div class="demo demo-3"> <button v-on:click="anim = !anim">{{anim}}</button> <transition name="anim"> <div v-if="anim">demo-3</div> </transition> </div>
export default { data() { return { anim: false }; }, };
用 vue 官方文档上有一张图说明整个生命周期web
另外一种是经过脚本控制动画效果浏览器
.demo-3, .demo-4 { div { top: 20px; } }
<div class="demo demo-4"> <button v-on:click="anim = !anim">{{anim}}</button> <transition v-on:before-enter="beforeEnter" v-on:enter="enter" v-on:after-enter="afterEnter" v-on:enter-cancelled="enterCancelled" v-on:before-leave="beforeLeave" v-on:leave="leave" v-on:after-leave="afterLeave" v-on:leave-cancelled="leaveCancelled" > <div v-if="anim">demo-4</div> </transition> </div>
export default { data() { return { anim: false }; }, methods: { beforeEnter(el) { console.warn('beforeEnter'); el.style = 'transition: left 2s;'; }, // 当与 CSS 结合使用时,回调函数 done 是可选的 enter(el, done) { console.warn('enter'); setTimeout(() => { el.style = 'transition: left 2s; left: 200px'; }); setTimeout(() => done(), 2000); }, afterEnter(el) { console.warn('afterEnter'); el.style = 'left: 200px;'; }, enterCancelled(el) { console.warn('enterCancelled'); }, beforeLeave(el) { console.warn('beforeLeave'); el.style = 'left: 200px;'; }, // 当与 CSS 结合使用时 // 回调函数 done 是可选的 leave(el, done) { console.warn('leave'); el.style = 'transition: left 2s;'; setTimeout(() => done(), 2000); }, afterLeave(el) { console.warn('afterLeave'); el.style = 'left: 0px;'; }, // leaveCancelled 只用于 v-show 中 leaveCancelled(el) { console.warn('leaveCancelled'); }, }, };
这种作法经过咱们在 transition 元素上绑定不一样的事件,经过控制回调中提供的 done方法 达到监听效果。app
transition 元素在vue中并不会生成 div 元素 有点像 template。
关键代码: src/platforms/web/runtime/components/transition.js
export default { name: 'transition', props: transitionProps, abstract: true, render (h: Function) { // ... 省略不少代码 const rawChild = children[0] // ... 省略不少代码 return rawChild } }
在 render 中直接返回了第一个子元素来渲染,具体的 patch 逻辑这里不作说明。
上面咱们展现了 transition 的两种监听动画的方法,下面看几段关键代码
src/platforms/web/runtime/modules/transition.js
const autoCssTransition: (name: string) => Object = cached(name => { return { enterClass: `${name}-enter`, leaveClass: `${name}-leave`, appearClass: `${name}-enter`, enterToClass: `${name}-enter-to`, leaveToClass: `${name}-leave-to`, appearToClass: `${name}-enter-to`, enterActiveClass: `${name}-enter-active`, leaveActiveClass: `${name}-leave-active`, appearActiveClass: `${name}-enter-active` } }) function resolveTransition (def?: string | Object): ?Object { // ... 省略不少代码 extend(res, autoCssTransition(def.name || 'v')) }
拼装 class 类名,以咱们传入的 name 属性 或者 v 开头,而且 name 与 v 后面的类名是固定的。
export function enter (vnode: VNodeWithData, toggleDisplay: ?() => void) { const el = vnode.elm // ... 省略不少代码 const startClass = isAppear ? appearClass : enterClass const activeClass = isAppear ? appearActiveClass : enterActiveClass const toClass = isAppear ? appearToClass : enterToClass const beforeEnterHook = isAppear ? (beforeAppear || beforeEnter) : beforeEnter const enterHook = isAppear ? (typeof appear === 'function' ? appear : enter) : enter // ... 省略不少代码 // 标记是否使用自定义样式控制css const expectsCSS = css !== false && !isIE9 // 标记用户是是否须要本身控制动画监听,也就是enter事件是否存在 const userWantsControl = enterHook && (enterHook._length || enterHook.length) > 1 // done 回调,用来手动结束动画效果 const cb = el._enterCb = once(() => { if (expectsCSS) { removeTransitionClass(el, toClass) removeTransitionClass(el, activeClass) } if (cb.cancelled) { if (expectsCSS) { removeTransitionClass(el, startClass) } enterCancelledHook && enterCancelledHook(el) } else { afterEnterHook && afterEnterHook(el) } el._enterCb = null }) if (!vnode.data.show) { // 插入元素时经过注入插入钩子, 调用enter事件 mergeVNodeHook(vnode.data.hook || (vnode.data.hook = {}), 'insert', () => { // ... 省略不少代码 // enterHook 调用的是在transition 传入的 enter 方法 enterHook && enterHook(el, cb) }, 'transition-insert') } beforeEnterHook && beforeEnterHook(el) // 使用样式控制的时候把 v-before-enter 与 v-enter样式加到dom元素上 if (expectsCSS) { addTransitionClass(el, startClass) addTransitionClass(el, activeClass) nextFrame(() => { addTransitionClass(el, toClass) removeTransitionClass(el, startClass) if (!cb.cancelled && !userWantsControl) { // 在元素上添加 transitionend监听 // 方法位于 transition-util.js 中 whenTransitionEnds(el, type, cb) } }) } // ... 省略不少代码 }
使用样式控制样式监听时经过添加和改变 dom 样式名以及 transitionend 达到监听效果。
手动监听动画时在元素插入时添加钩子提供回调函数以达到监听效果。
与 enter 对应的 leave 逻辑其实都差很少,这里不作过多讲解。
以上篇幅只是一个初步简略分析,时间有限,不少细节并未深究。
以上内容鉴于 vue 2.18 版本,其余版本可能会有所改动。
https://developer.mozilla.org/zh-CN/docs/Web/Events/transitionend
https://cn.vuejs.org/v2/guide/transitions.html