在幾年前 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