共计 6438 个字符,预计需要花费 17 分钟才能阅读完成。
前言
在几年前 jQuery 流行的时候大家都通过 js 去操作 dom 元素的 css 来实现以及监听动画,甚至出现了很多通过 js 去监听动画的动画库。前端时间在写 vue 的时候发现 vue 中实现动画效果,并没有通过 js 去不停的操作 css 样式,那么在 css 中是怎么去监听 dom 元素的动画效果呢?
纯 js 动画监听示例
实现下图中的动画效果监听:
#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/pqRgOe
实现效果
公共样式长这样
.demo {
height: 120px;
position: relative;
div {
position: absolute;
background: red;
width: 100px;
height: 100px;
left: 0;
top: 0;
}
}
vue transitionend
<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.js
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”上,以达到动画监听效果。
transition
有两种用法,一种是通过 css 控制动画效果
.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 官方文档上有一张图说明整个生命周期
另一种是通过脚本控制动画效果
.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 方法 达到监听效果。
transition 元素
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 动画控制源码
上面我们展示了 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/transitionendhttps://cn.vuejs.org/v2/guide/transitions.html