vue 動畫監聽簡略分析

前言

在幾年前 jQuery 流行的時候大家都通過js去操作dom元素的css來實現以及監聽動畫,甚至出現了很多通過js去監聽動畫的動畫庫。
前端時間在寫 vue 的時候發現 vue 中實現動畫效果,並沒有通過 js 去不停的操作css樣式,那麼在css中是怎麼去監聽dom元素的動畫效果呢?

純js動畫監聽示例

實現下圖中的動畫效果監聽:
clipboard.png

#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

實現效果

clipboard.png

公共樣式長這樣

.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 官方文檔上有一張圖說明整個生命週期
clipboard.png

另一種是通過腳本控制動畫效果

.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/transitionend
https://cn.vuejs.org/v2/guide/transitions.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章