vue 动画监听简略分析

2

前言

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

你可能感兴趣的

载入中...