Vue3.0高阶组件之Transition

3,250 阅读3分钟

对于Vue 3.0 Transition高阶组件仰慕已久,一直很想知道是Transition组件是如何工作的,下面一步一步解析它是如何添加和删除我们常用的动画class。

Vue 提供了 transition 的封装组件,在下列情形中,可以给任何元素和组件添加进入/离开过渡:

  • 条件渲染 (使用 v-if)
  • 条件展示 (使用 v-show)
  • 动态组件
  • 组件根节点

Transition如何嗅探是否应用了 CSS 过渡或动画

条件渲染时,Vue在patch函数中mountElement时,通过下例方法来嗅探transition。

// parentSuspense 和 Suspense组件相关,暂时不考虑
 const monuntElement = function (vnode, //...) {
   const { transition } = vnode
   // ...
   // 判断是否使用transition
   const needCallTransitionHooks = 
     (!parentSuspense || (parentSuspense && !parentSuspense.pendingBranch)) &&
     transition && !transition.persisted
   // 通过beforEnter钩子函数初始化动画
   if (needCallTransitionHooks) {
      transition!.beforeEnter(el)
   }
   // ...
 }
 
 

transition通过resolveTransitionHooks方法解析出对内的钩子函数,通过setTransitionHooks方法添加到vnode的transition属性

// BaseTransition.ts
const BaseTransitionImpl = {
  setup (props, { slots }) {
    return (() => {
      // ...
      // 过渡钩子作为 vnode.transition 附加到 vnode
      const enterHooks = resolveTransitionHooks(
        innerChild,
        rawProps,
        state,
        instance
      )
      setTransitionHooks(child, enterHooks)
      // ...
    })
  }
}

function setTransitionHooks (vnode, hooks) {
  if (vnode.shapeFlag & ShapeFlags.COMPONENT && vnode.component) {
    setTransitionHooks(vnode.component.subTree, hooks)
  } else if (__FEATURE_SUSPENSE__ && vnode.shapeFlag & ShapeFlags.SUSPENSE) {
    vnode.ssContent!.transition = hooks.clone(vnode.ssContent!)
    vnode.ssFallback!.transition = hooks.clone(vnode.ssFallback!)
  } else {
    vnode.transition = hooks // 设置hooks
  }
}

resolveTransitionHooks() => TransitionHooks 方法提供enter、leave等方法来处理钩子函数

interface TransitionHooks {
  mode: BaseTransitionProps['mode']
  persisted: boolean
  beforeEnter(el: HostElement): void
  enter(el: HostElement): void
  leave(el: HostElement, remove: () => void): void
  clone(vnode: VNode): TransitionHooks<HostElement>
  // optional
  afterLeave?(): void
  delayLeave?(
    el: HostElement,
    earlyRemove: () => void,
    delayedLeave: () => void
  ): void
  delayedLeave?(): void
}

Transition类名如何在适当的时机添加

当调用TransitionHooks时,会触发resolveTransitionProps方法产生的Props对应的钩子函数在适当的时机来添加transition类名,类似我们的v-enter-active,v-from-active…,最后通过 whenTransitionEnds 方法来结束transition动画

export const Transition = (props, { slots }) => 
  h(BaseTransition, resolveTransitionProps(props), slots)
@runtime-dom/src/components/Transition
// 内容很多,可自行查看
function resolveTransitionProps() {
  // ...
  // 如何添加动画Class 用enter举例
  const makeEnterHook = (isAppear) => {
    return (el, done) => {
      const hook = isAppear ? onAppear : onEnter;
      const resolve = () => finishEnter(el, isAppear, done);
      hook && hook(el, resolve); // 调用使用时传入的钩子函数
      nextFrame(() => {
          removeTransitionClass(el, isAppear ? appearFromClass : enterFromClass);
          addTransitionClass(el, isAppear ? appearToClass : enterToClass);
          if (!(hook && hook.length > 1)) {
              whenTransitionEnds(el, type, enterDuration, resolve);
          }
      });
   };
   return {
    // ...
    onEnter: makeEnterHook(false)
   }
 };
  
}

添加/删除transitionClass方法

function addTransitionClass(el, cls) {
  cls.split(/\s+/).forEach(c => c && el.classList.add(c));
  (el._vtc ||
      (el._vtc = new Set())).add(cls);
}
function removeTransitionClass(el, cls) {
  cls.split(/\s+/).forEach(c => c && el.classList.remove(c));
  const { _vtc } = el;
  if (_vtc) {
      _vtc.delete(cls);
      if (!_vtc.size) {
          el._vtc = undefined;
      }
  }
}

vShow条件展示则是在vShow的指令中触发transition的钩子函数

export const vShow = {
  beforeMount(el, { value }, { transition }) {
    el._vod = el.style.display === 'none' ? '' : el.style.display
    if (transition && value) {
      transition.beforeEnter(el) // before
    } else {
      setDisplay(el, value)
    }
  },
  mounted(el, { value }, { transition }) {
    if (transition && value) {
      transition.enter(el) // enter
    }
  },
  updated(el, { value, oldValue }, { transition }) {
    // update
    if (transition && value !== oldValue) {
      if (value) {
        transition.beforeEnter(el)
        setDisplay(el, true)
        transition.enter(el)
      } else {
        transition.leave(el, () => {
          setDisplay(el, false)
        })
      }
    } else {
      setDisplay(el, value)
    }
  },
  beforeUnmount(el, { value }) {
    setDisplay(el, value)
  }
}

卸载钩子函数及TransitionClass

当组件进行到unmount方法时,会调用remove方法来卸载组件的transition动画。vShow则是在update指令中进行。

const remove = vnode => {
  const { type, el, anchor, transition } = vnode;
  // ...
  const performRemove = () => {
      hostRemove(el);
      if (transition && !transition.persisted && transition.afterLeave) {
          transition.afterLeave();
      }
  };
  if (vnode.shapeFlag & 1 /* ELEMENT */ &&
      transition &&
      !transition.persisted) {
      const { leave, delayLeave } = transition; // 添加 leave-transition-class
      const performLeave = () => leave(el, performRemove);
      if (delayLeave) {
          delayLeave(vnode.el, performRemove, performLeave);
      }
      else {
          performLeave();
      }
  }
  else {
      performRemove();
  }
};

总结

本次查看源码大致的知道了Transition的渲染过程,它自身不会渲染一个 DOM 元素,也不会出现在父组件链中,而是render一个子节点。Transition的代码实现很巧妙,通过childVnode的transition属性值来处理动画的进入/离开,真的很有意思,值得学习和思考。

以上是本次Transition的分享,Transition组件内容很丰富。个人理解有限,希望对大家有帮助。如有错漏,请大家指正。