手摸手实现Transition
xdm
好,我是剑大瑞。
本篇内容旨在通过本人实现Transition
组件,从而理解其外部原理。
如果你还没有应用过Transition
组件或者对其不相熟,那么我倡议你能够先学习官网文档,写一些demo
,当相熟了Transition
组件之后,然而又对其原理有所好奇,就能够再回来学习这篇文章。官网文档传送门。
前言
通过官网文档能够晓得,当应用Transition
组件的时候,咱们能够通过配置Transition
组件的props
管制组件的进场过渡、离场过渡状态、动画成果。
配置props
的过程中,重要的是指定name
。Vue
会将name
字段与不同的过渡阶段名称进行组合,在不同的阶段为咱们的dom
增加类名或者移除类名。
这里借用官网的示意图:
这张图片对于Transition
组件的过渡成果形容十分确切了:
- 当组件挂载的时候,
class
由v-enter-from
过渡为v-enter-to
。切换的两头过程咱们称它为v-enter-active
。 - 当组件卸载的时候,
class
由v-leave-from
过渡为v-leave-to
。切换的过程咱们称它为v-leave-active
。 - 在由
enter-from⇒enter-to
或者leave-from⇒leave-to
的阶段,咱们能够指定组件的初始和最终款式。在enter-active
&leave-active
阶段咱们能够指定组件的过渡或者动画成果。
首先咱们须要调用defineComponent
API来定义一个MyTransition
组件,通过setup
获取插槽中的内容。
这外面有两点须要思考:
MyTransition
只会把过渡成果利用到其包裹的内容上,而不会额定渲染DOM
元素,也不会呈现在可被查看的组件层级中。就是说组件并不需要有本人的
template
,只做插槽的搬用工。MyTransition
组件并不需要有本人的状态,只需将用户传入的props
解决后,再将解决后的newProps
传给子组件即可。就是说
MyTransition
组件并不需要有本人的状态,只做状态的搬运工。
Props
设计
然而咱们怎么设计props
呢?
思考这个问题,还须要回到Transition
组件的外围逻辑在于:
- 在组件的挂载阶段,咱们须要将
enter-from
至enter-to
阶段的过渡或者动画成果class
附加到DOM
元素上。 - 在组件的卸载阶段,咱们须要将
leave-from
至leave-to
阶段的过渡或者动画成果class
附加到DOM
元素上。
那咱们是否须要通过mounted
、unmounted
API钩子中实现class
的移除和增加呢?
答案:其实不须要。在Vue
中的Transition
组件是与渲染器的patch
逻辑高度依赖的。
渲染器解决形式
在渲染器中,能够在mountElement
函数中,解决Enter
阶段的过渡或者动画成果。在remove
函数中解决Leave
阶段的过渡或者动画成果。
这里咱们在此简略看下这两个函数的代码:
mountElement
函数简略版,mountElement
函数负责挂载元素。
// 挂载元素节点const mountElement = (vnode,...args) => { let el; let vnodeHook; const { type, props, shapeFlag, transition, patchFlag, dirs } = vnode; // 省略局部代码... *if (needCallTransitionHooks*) { // 执行过渡钩子 transition.beforeEnter(el); } // 省略局部代码... if ((vnodeHook = props && props.onVnodeMounted) || needCallTransitionHooks || dirs) { // post 各种钩子 至后置执行工作池 queuePostRenderEffect(() => { // 执行过渡动画钩子 needCallTransitionHooks && transition.enter(el); }, parentSuspense); }};
remove
函数简略版,remove
函数次要负责从父元素中移除元素。
// 移除Vnodeconst remove = vnode => { const { type, el, anchor, transition } = vnode; // 省略局部代码... const performRemove = () => { hostRemove(el); if (transition && !transition.persisted && transition.afterLeave) { // 执行transition钩子 transition.afterLeave(); } }; if (vnode.shapeFlag & 1 /* ELEMENT */ && transition && !transition.persisted) { const { leave, delayLeave } = transition; // 执行lea const performLeave = () => leave(el, performRemove); if (delayLeave) { delayLeave(vnode.el, performRemove, performLeave); } else { performLeave(); } }};
move
函数简略版,move
函数次要负责元素的挪动,插入父元素。
const move = (vnode, container, anchor, moveType, parentSuspense = null) => { const { el, type, transition, children, shapeFlag } = vnode; // 省略局部代码... if (needTransition) { if (moveType === 0 /* ENTER */) { // 执行过渡钩子 transition.beforeEnter(el); hostInsert(el, container, anchor); queuePostRenderEffect(() => transition.enter(el), parentSuspense); } else { const { leave, delayLeave, afterLeave } = transition; const remove = () => hostInsert(el, container, anchor); const performLeave = () => { leave(el, () => { remove(); afterLeave && afterLeave(); }); }; if (delayLeave) { delayLeave(el, remove, performLeave); } else { performLeave(); } } } // 省略局部代码...};
通过下面的代码,能够晓得,Vue3
是通过渲染器执行Transition
组件自定义的钩子函数,来实现过渡成果的管制的。
所以咱们能够通过为props
定义钩子函数,并绑定到transition
组件,在元素的patch
阶段,执行钩子函数,从而实现对动效的管制。
Javascript
钩子解决props
为此咱们能够参考官网文档中的JavaScript钩子局部,为props
定义Enter
& Appear
& Leave
阶段的钩子。
在钩子函数中操作动效class
的移除或增加操作。
const MyTransition = defineComponent({ name: 'MyTransition', props: { name: { type: String, default: 'v' }, type: String, css: { type: Boolean, default: true }, duration: [String, Number, Object], enterFromClass: String, enterActiveClass: String, enterToClass: String, appearFromClass: String, appearActiveClass: String, appearToClass: String, leaveFromClass: String, leaveActiveClass: String, leaveToClass: String }, setup(props, { slots }) { const children = slots.default() const newProps = {} for (const key in props) { newProps[key] = props[key] } const { name = 'v', type, duration, enterFromClass = `${name}-enter-from`, enterActiveClass = `${name}-enter-active`, enterToClass = `${name}-enter-to`, appearFromClass = enterFromClass, appearActiveClass = enterActiveClass, appearToClass = enterToClass, leaveFromClass = `${name}-leave-from`, leaveActiveClass = `${name}-leave-active`, leaveToClass = `${name}-leave-to` } = props // 为newProps绑定够子函数 Object.assign(newProps, { // Enter阶段 onBeforeEnter(el) { }, onEnter(el) { }, onAfterEnter(el) { }, onEnterCancelled(el) { }, // Apear阶段 onBeforeAppear(el) { }, onAppear(el) { }, onAppearCancelled(el) { }, // Leave阶段 onLeave(el) { }, onLeaveCancelled(el) { }, }) // 为子元素绑定通过解决的newProps return h(children, newProps, null) }})
通过下面的代码,能够晓得,通过解构props
,组合成各动效阶段的class
。
钩子函数都会承受一个el
参数,它代表以后须要进行增加过渡动效的DOM
,由渲染器在patch
阶段传入。
接下来的工作就是在JavaScript
钩子函数中,操作class
。
欠缺钩子函数
Javascript
钩子函数的主要职责是为el
增加或者移除动效class
。
然而咱们须要先明确每个类应该在何时增加?何时移除?
在进入/来到的过渡中,会有 6 个 class
切换。
v-enter-from
:定义进入过渡的开始状态。在元素被插入之前失效,在元素被插入之后的下一帧移除。v-enter-active
:定义进入过渡失效时的状态。在整个进入过渡的阶段中利用,在元素被插入之前失效,在过渡/动画实现之后移除。这个类能够被用来定义进入过渡的过程工夫,提早和曲线函数。v-enter-to
:定义进入过渡的完结状态。在元素被插入之后下一帧失效 (与此同时v-enter-from
被移除),在过渡/动画实现之后移除。v-leave-from
:定义来到过渡的开始状态。在来到过渡被触发时立即失效,下一帧被移除。v-leave-active
:定义来到过渡失效时的状态。在整个来到过渡的阶段中利用,在来到过渡被触发时立即失效,在过渡/动画实现之后移除。这个类能够被用来定义来到过渡的过程工夫,提早和曲线函数。v-leave-to
:来到过渡的完结状态。在来到过渡被触发之后下一帧失效 (与此同时v-leave-from
被移除),在过渡/动画实现之后移除。
由此可知,咱们须要:
- 在
onBeforeEnter
函数中实现enterFromClass
&enterActiveClass
增加工作。 - 在
onEnter
函数中实现下一帧绘制的间隙,实现enterFromClass
的移除,enterToClass
的增加工作。 - 当
Enter
阶段的动画完结之后须要实现enterActiveClass
&enterToClass
移除工作。
为了不便class
的增加 || 移除操作咱们能够先定义两个用于操作class
的函数,不便在多个钩子中应用。
// 增加类function addTransitionClass(el, cls) { cls.split(/\s+/).forEach(c => c && el.classList.add(c))}// 移除类function removeTransitionClass(el, cls) { cls.split(/\s+/).forEach(c => c && el.classList.remove(c))}
通过下面两个函数,能够实现onBeforeEnter
& onEnter
钩子:
setup() { // 省略局部代码... Object.assign(baseProps, { // 传入通过解决后的 props // Enter onBeforeEnter(el) { // 增加class... addTransitionClass(el, enterFromClass) addTransitionClass(el, enterActiveClass) }, onEnter(el) { // 在下一帧执行的时候移除class requestAnimationFrame(() => { // 移除enterFromClass removeTransitionClass(el, enterFromClass) // 而后增加新的enterToClass addTransitionClass(el, enterToClass) }) }, // 省略局部代码... })}
两个问题
下面的代码会有两个问题:
requestAnimationFrame
中的回调函数真的能如咱们所冀望的那样在下一帧中执行吗?- 如何实现动效完结之后,对
class
的移除?
先说第一个问题,答案是否定的。requestAnimationFrame
中的回调,会在以后帧就实现执行。那是为什么呢?
通过查阅MDN,能够晓得。通过requestAnimationFrame
注册的回调函数通常会在浏览器下一次重绘之前执行,而不是在下一帧中执行。
如果想在浏览器下次重绘之前持续更新下一帧动画,那么回调函数本身必须再次调用window.requestAnimationFrame()
为了实现在下一帧中对class
的移除 && 增加。须要将onEnter
中的代码改写为:
setup() { // 省略局部代码... Object.assign(baseProps, { onEnter(el) { // 在下一帧执行的时候移除class requestAnimationFrame(() => { requestAnimationFrame(() => { // 移除enterFromClass removeTransitionClass(el, enterFromClass) // 而后增加新的enterToClass addTransitionClass(el, enterToClass) }) }) }, // 省略局部代码... })}
第二个问题:移除动效class
。当为DOM
增加class
之后,就会触动员效。触发之后咱们能够通过监听
transitionend
事件或者animationend
事件,而后移除动效class
。
持续改写onEnter
函数:
onEnter(el) { // 定义一个供addEventListener执行的回调函数 const resolve = () => { removeTransitionClass(el, enterToClass) removeTransitionClass(el, enterActiveClass) } // 在下一帧执行的时候移除class requestAnimationFrame(() => { requestAnimationFrame(() => { removeTransitionClass(el, enterFromClass) addTransitionClass(el, enterToClass) // 监听动效完结事件,type由props传入 el.addEventListener(`${type}end`, resolve) }) })},// 省略局部代码...
至此就实现Enter
阶段的两个钩子函数。
同样的逻辑,咱们能够实现Leave
阶段的钩子函数。
onLeave(el) { // 定义resolve回调 const resolve = () => { removeTransitionClass(el, leaveToClass) removeTransitionClass(el, leaveActiveClass) } // 间接增加leaveFromClass addTransitionClass(el, leaveFromClass) addTransitionClass(el, leaveActiveClass) // 来到阶段的下一帧中移除class requestAnimationFrame(() => { requestAnimationFrame(() => { removeTransitionClass(el, leaveFromClass) addTransitionClass(el, leaveToClass) el.addEventListener(`${type}end`, resolve) }) })}
与Enter
阶段不同的是Leave
阶段的fromClass
& activeClass
并没有在beforeOnLeave
阶段进行,而是间接在onLeave
阶段开始。
这就有一个问题,咱们间接增加的leaveFromClass
并不能让动效立刻失效,这波及到一个issue
相干链接
- issue: https://github.com/vuejs/core...
- 复现链接:https://codesandbox.io/s/comp...
其粗心是:当在初始阶段通过state
管制元素的style
做暗藏或者显示时,Transition
组件Leave
阶段动效并没有按合乎预期的成果进行转换。
为此咱们须要在增加了leaveFromClass
后,通过强制触发一次强制reflow
,使 -leave-from
classes
能够立刻失效。
onLeave(el, done) { const resolve = () => { removeTransitionClass(el, leaveToClass) removeTransitionClass(el, leaveActiveClass) } // 间接增加leaveFromClass addTransitionClass(el, leaveFromClass) // 通过读取offsetHeight实现强制reflow document.body.offsetHeight addTransitionClass(el, leaveActiveClass) requestAnimationFrame(() => { requestAnimationFrame(() => { removeTransitionClass(el, leaveFromClass) addTransitionClass(el, leaveToClass) el.addEventListener(`${type}end`, resolve) }) })}
onLeaveCancelled
钩子仅用于v-show
中,会勾销leaveActive
& leaveTo
的动效。这个实现并不简单。
onLeaveCancelled(el) { removeTransitionClass(el, leaveToClass) removeTransitionClass(el, leaveActiveClass)}
至此,咱们曾经实现了Enter
& Leave
阶段的动效钩子实现。
接下来还须要实现Appear
阶段的钩子函数。Appear
钩子函数的调用逻辑为当用户为props
配置了appear = true
时,则会在初始渲染阶段就出动员效。
其实现逻辑与Enter
阶段根本一样:
onBeforeAppear(el) { addTransitionClass(el, appearFromClass) addTransitionClass(el, appearActiveClass)},onAppear(el: Element) { // 定义resolve函数 const resolve = () => { removeTransitionClass(el, appearToClass) removeTransitionClass(el, appearActiveClass) } // 在下一帧执行的时候移除class // 如果isApper为true移除from否则移除enter requestAnimationFrame(() => { requestAnimationFrame(() => { removeTransitionClass(el, appearFromClas) addTransitionClass(el, appearToClass ) el.addEventListener(`${type}end`, resolve) }) })}onAppearCancelled(el) { removeTransitionClass(el, appearToClass) removeTransitionClass(el, appearActiveClass)},
至此咱们曾经实现了Enter Appear Leave
阶段的钩子定义。然而会发现代码中会有很多冗余。代码逻辑有很多反复之处。为此咱们能够将代码进行优化。
重构
- 将过渡开始须要增加
class
的局部抽离为startBefore
,将过渡完结后须要移除class
的局部抽离为finishEnter
、finishLeave
函数,通过参数isAppear
来判断增加或者移除哪些class
。
const startBefore = (el, isAppear) => { addTransitionClass(el, isAppear ? appearFromClass : enterFromClass); addTransitionClass(el, isAppear ? appearActiveClass : enterActiveClass);};const finishEnter = (el, isAppear) => { removeTransitionClass(el, isAppear ? appearToClass : enterToClass); removeTransitionClass(el, isAppear ? appearActiveClass : enterActiveClass);};const finishLeave = (el) => { removeTransitionClass(el, leaveToClass); removeTransitionClass(el, leaveActiveClass);};
- 将嵌套的
requestAnimationFrame
抽离为nextFrame
函数。
function nextFrame(cb) { requestAnimationFrame(() => { requestAnimationFrame(cb); });}
- 将监听
transitionend
&animationend
事件的逻辑抽离为whenTransitionEnds
函数
function whenTransitionEnds(el, type, resolve) { const endEvent = type + ‘end’ const end = () => { // 每次监听时,先移除原有的监听事件 el.removeEventListener(endEvent, onEnd); resolve(); }; const onEnd = (e) => { if (e.target === el) { end(); } }; el.addEventListener(endEvent, onEnd);}
onEnter
与onAppear
函数逻辑存在反复之处,咱们能够定义一个高阶函数,用于返回钩子函数。
const makeEnterHook = (isAppear) => { return (el) => { const hook = isAppear ? onAppear : onEnter; const resolve = () => finishEnter(el, isAppear, done); nextFrame(() => { removeTransitionClass(el, isAppear ? appearFromClass : enterFromClass; addTransitionClass(el, isAppear ? appearToClass : enterToClass); whenTransitionEnds(el, type, resolve); });}
调用函数重构MyTransition
:
function whenTransitionEnds(el, type, resolve) { const endEvent = type + ‘end’ const end = () => { // 每次监听时,先移除原有的监听事件 el.removeEventListener(endEvent, onEnd); resolve(); }; const onEnd = (e) => { if (e.target === el) { end(); } }; el.addEventListener(endEvent, onEnd);}function nextFrame(cb) { requestAnimationFrame(() => { requestAnimationFrame(cb); });}const MyTransition = defineComponent({ name: 'MyTransition', props: { name: { type: String, default: 'v' }, type: String, css: { type: Boolean, default: true }, duration: [String, Number, Object], enterFromClass: String, enterActiveClass: String, enterToClass: String, appearFromClass: String, appearActiveClass: String, appearToClass: String, leaveFromClass: String, leaveActiveClass: String, leaveToClass: String }, setup(props, { slots }) { const children = slots.default() const newProps = {} const { name = 'v', type, duration, enterFromClass = `${name}-enter-from`, enterActiveClass = `${name}-enter-active`, enterToClass = `${name}-enter-to`, appearFromClass = enterFromClass, appearActiveClass = enterActiveClass, appearToClass = enterToClass, leaveFromClass = `${name}-leave-from`, leaveActiveClass = `${name}-leave-active`, leaveToClass = `${name}-leave-to` } = props const startBefore = (el, isAppear) => { addTransitionClass(el, isAppear ? appearFromClass : enterFromClass); addTransitionClass(el, isAppear ? appearActiveClass : enterActiveClass); }; const finishEnter = (el, isAppear) => { removeTransitionClass(el, isAppear ? appearToClass : enterToClass); removeTransitionClass(el, isAppear ? appearActiveClass : enterActiveClass); }; const finishLeave = (el) => { removeTransitionClass(el, leaveToClass); removeTransitionClass(el, leaveActiveClass); }; const makeEnterHook = (isAppear) => { return (el) => { const hook = isAppear ? onAppear : onEnter; const resolve = () => finishEnter(el, isAppear, done); nextFrame(() => { removeTransitionClass(el, isAppear ? appearFromClass : enterFromClass; addTransitionClass(el, isAppear ? appearToClass : enterToClass); whenTransitionEnds(el, type, resolve) }); } Object.assign(newProps, { onBeforeEnter(el) { startBefore(el, false) }, onBeforeAppear(el) { startBefore(el, true) }, onEnter: makeEnterHook(false), onAppear: makeEnterHook(true), onLeave(el) { const resolve = () => finishLeave(el); addTransitionClass(el, leaveFromClass); document.body.offsetHeight; addTransitionClass(el, leaveActiveClass); nextFrame(() => { removeTransitionClass(el, leaveFromClass); addTransitionClass(el, leaveToClass); whenTransitionEnds(el, type, resolve); }); }, onEnterCancelled(el) { finishEnter(el, false); }, onAppearCancelled(el) { finishEnter(el, true); }, onLeaveCancelled(el) { finishLeave(el); } }) return h(children, newProps, null) }})
通过重构后,代码简洁了很多。在来张图片总结下上述过程。
持续时间实现
这里还有一个小性能须要实现,就是设置显性的过渡持续时间。
当用户设置duration
属性的时候,能够使其中一些嵌套的外部元素相比于过渡成果的根元素具备提早的或更长的过渡成果。
应用的时候,你能够用 <transition>
组件上的 duration
prop 显式指定过渡持续时间 (以毫秒计):
<transition :duration="1000">...</transition>
你也能够别离指定进入和来到的持续时间:
<transition :duration="{ enter: 500, leave: 800 }">...</transition>
这意味着,用户能够传单个工夫或者以对象的模式,执行Enter
阶段 & Leave
阶段的过渡工夫。
那如何实现一个继续的成果呢?
让咱们回顾先原来的逻辑。
在通常状况下,咱们会通过监听transitionend
|| animationend
事件。来移除动效class
。当初咱们须要期待durationTime
之后能力移除。
那咱们能够期待duration
之后,再移除动效class
。能够应用setTimeout
来实现这个继续成果,只需将durationTime
传入whenTransitionEnds
函数。whenTransitionEnds
函数通过调用setTimeout
来开启一个延时工作,期待duration
之后,执行移除class
的回调。接下来略微调整一下代码逻辑即可。
// 定义normalizeDuration函数function normalizeDuration(duration) { if (duration == null) { return null; } else if (isObject(duration)) { return [NumberOf(duration.enter), NumberOf(duration.leave)]; } else { const n = NumberOf(duration); return [n, n]; }}// 在setup函数中,对duration进行标准解决const durations = normalizeDuration(duration)const enterDuration = durations && durations[0]const leaveDuration = durations && durations[1]
改写makeEnterHook
&& onLeave
&& whenTransitionEnds
函数:
const makeEnterHook = (isAppear) => { return (el) => { // 省略局部代码... whenTransitionEnds(el, type, enterDuration, resolve) }}onLeave(el) { // 省略局部代码... whenTransitionEnds(el, type, leaveDuration, resolve)}function whenTransitionEnds(el, type, explicitTimeout,resolve) { // 省略局部代码... const resolveIfNotStale = () => { resolve() } // 如果存在继续过渡工夫,间接通过setTimeout来判断 if (explicitTimeout) { return setTimeout(resolveIfNotStale, explicitTimeout) } // 省略局部代码... const end = () => { // 每次监听时,先移除原有的监听事件 el.removeEventListener(endEvent, onEnd); resolveIfNotStale(); }; const onEnd = (e) => { if (e.target === el) { end(); } }; el.addEventListener(endEvent, onEndd)}
通过改写whenTransitionEnds
函数能够晓得,当设置duration
时,先判断explicitTimeout
是否存在,如果存在,间接通过setTimeout
来实现提早移除class
。
JavaScript
钩子实现
Vue
的Transition
组件除了能够应用css
来管制组件的动效,还能够通过JavaScript
来管制。
当动效须要应用JavaScript
管制时,须要在methods
中配置相应的钩子函数。
如果须要通过JavaScript
管制整个动效过程,须要在props
中设置,css = false
。
然而再开始JavaScript
钩子之前,咱们做一些调整。
通过后面的代码,能够发现,咱们的MyTransition
的大部分逻辑其实是在解决props
,定义钩子函数。
拆散
接下来为了让代码不那么臃肿,咱们能够在设计一个MyTransitionBase
组件,该组件次要负责:
- 将钩子函数挂载至
DOM
上 - 实现动效过渡模式
开始吧。
// 定义钩子类型校验const TransitionHookValidator = [Function, Array];const MyTransitionBase = defineComponent({ name: `MyTransitionBase`, props: { mode: String, appear: Boolean, // enter onBeforeEnter: TransitionHookValidator, onEnter: TransitionHookValidator, onAfterEnter: TransitionHookValidator, onEnterCancelled: TransitionHookValidator, // leave onBeforeLeave: TransitionHookValidator, onLeave: TransitionHookValidator, onAfterLeave: TransitionHookValidator, onLeaveCancelled: TransitionHookValidator, // appear onBeforeAppear: TransitionHookValidator, onAppear: TransitionHookValidator, onAfterAppear: TransitionHookValidator, onAppearCancelled: TransitionHookValidator }, setup(props, { slots }) { // 返回一个渲染函数 return () => { // 获取子节点 const children = slots.default if (!children || !children.length) { return; } // 只为单个元素/组件绑定过渡成果 const child = children[0]; // 接下来在这里实现子节点钩子函数挂载和设置过渡模式的实现 return child; }; }};)
咱们须要再解决下MyTransition
组件。MyTransition
组件仅负责props的解决,在MyTransition
组件中,会将class
动效转为JavaScript
动效钩子,如果用户告诉绑定JavaScript
钩子,只需在Javascript
钩子函数中调用配置的钩子即可。
import { h } from 'vue'// 将MyTransition转为函数式组件const MyTransition = (props, { slots }) => h(MyTransitionBase, resolveMyTransitionProps(props), slots);// 定义一个callHook函数用于执行JavaScript钩子const callHook = (hook, args = []) => { if (isArray(hook)) { hook.forEach(h => h(...args)); } else if (hook) { hook(...args); }};// 定义resolveMyTransitionProps,负责props解决function resolveMyTransitionProps(rawProps) { const newProps = {}; // 将rawProps上的属性全副从新绑定至newProps for (const key in rawProps) { newProps[key] = rawProps[key]; } // 如果仅应用javascript钩子管制动效,那么间接返回newProps if (rawProps.css === false) { return newProps; } // 省略局部代码... // 解构出JavaScript钩子 const { onBeforeEnter, onEnter, onEnterCancelled, onLeave, onLeaveCancelled, onBeforeAppear = onBeforeEnter, onAppear = onEnter, onAppearCancelled = onEnterCancelled } = newProps; const makeEnterHook = (isAppear) => { return (el, done) => { // 省略局部代码... callHook(hook, [el, resolve]) }; }; return extend(newProps, { onBeforeEnter(el) { // 省略局部代码... callHook(onBeforeEnter, [el]); }, onBeforeAppear(el) { // 省略局部代码... callHook(onBeforeAppear, [el]); }, onEnter: makeEnterHook(false), onAppear: makeEnterHook(true), onLeave(el, done) { // 省略局部代码... callHook(onLeave, [el, resolve]); }, onEnterCancelled(el) { // 省略局部代码... callHook(onEnterCancelled, [el]); }, onAppearCancelled(el) { // 省略局部代码... callHook(onAppearCancelled, [el]); }, onLeaveCancelled(el) { // 省略局部代码... callHook(onLeaveCancelled, [el]); } });}
下面代码省略的局部为原来就有的,调整的只是新增的局部。
从下面的代码,能够发现:
- 因为在后面咱们说过
MyTransition
组件没有本人的状态,所以咱们能够通过渲染函数将其定义为一个函数式组件。 - 定义了一个
resolveMyTransitionProps
函数,用于做props
的解决。 - 如果用于配置的
css = false
,能够间接返回newProps
。 - 用户同时应用
css & JavaScript
钩子实现动效时,须要callHook
函数调用解构进去的钩子函数。
解决MyTransitionBase
拆分后MyTransitionBase
组件次要负责JavaScript
钩子的调用。
MyTransition
组件为class
动效与JavaScript
钩子做了层兼容合并解决,最终都以JavaScript
钩子的模式传递给MyTransitionBase
组件。
接下来咱们在MyTransitionBase
组件中实现Javascipt
钩子与子节点的绑定。
然而在绑定之前,咱们须要在剖析下Enter
阶段 & Appear
阶段动效的区别和分割。
Appear
阶段的钩子调用次要通过用户是否为props
配置appear
属性判断。
appear
属性用于判断是否在初始渲染时应用动效。
在通常状况下,appear = false
。
当用户为appear = true
时,会在初始阶段就利用动效。
那么咱们如何判断是初始阶段呢?
这里也不再绕弯子了。咱们能够在MyTransitionBase
组件beforeEnter
& enter
阶段 钩子中,通过判断是否MyTransitionBase
曾经mounted
,来判断是否是初始渲染状态。
如果没有挂载,则咱们在beforeEnter
钩子中执行props
中传递的onBeforeEnter
钩子即可。
如果曾经实现挂载,并且用户传递的appear
= true
,则执行onBeforeAppear
|| onBeforeEnter
。
同样的逻辑实用于enter
阶段:
MyTransitionBase
组件挂载执行onEnter
钩子- 否则执行
onAppear
钩子
import { onMounted, onBeforeUnmount } from 'vue'const MyTransitionBase = defineComponent({ // 省略局部代码... setup(props, { slots }) { // 定义一个state用于记录MyTransitionBase是否实现挂载 | 卸载 const state = { isMounted: false, isUnmounting: false, } onMounted(() => { state.isMounted = true }) onBeforeUnmount(() => { state.isUnmounting = true }) // 返回一个渲染函数 return () => { // 获取子节点 const children = slots.default if (!children || !children.length) { return; } // 只为单个元素/组件绑定过渡成果 const child = children[0]; // 获取Enter阶段钩子 const hooks = resolveTransitionHooks( child, props, state ) // 将钩子绑定至子节点的 transition 属性 // 当渲染器渲染的时候会调用Hooks setTransitionHooks(child, hooks) return child; }; }})
定义resolveTransitionHooks
函数,负责解析动效hooks
。
// 负责解析Hooksfunction resolveTransitionHooks(vnode, props, state) { const { appear, mode, persisted = false, onBeforeEnter, onEnter, onAfterEnter, onEnterCancelled, onBeforeLeave, onLeave, onAfterLeave, onLeaveCancelled, onBeforeAppear, onAppear, onAfterAppear, onAppearCancelled } = props; const hooks = { mode, persisted, beforeEnter(el) { let hook = onBeforeEnter; if (!state.isMounted) { // 依据用户属性判断是否应用onBeforeAppear // 如果用户没有传onBeforeAppear则应用onBeforeEnter if (appear) { hook = onBeforeAppear || onBeforeEnter; } else { return; } } callHook(hook, [el]); }, enter(el) { let hook = onEnter; if (!state.isMounted) { if (appear) { hook = onAppear || onEnter; } else { return; } } if (hook) { hook(el); } }, leave(el) { callHook(onBeforeLeave, [el]); if (onLeave) { onLeave(el); } } }; return hooks;}
定义函数,用于将hooks
绑定至Vnode
// 用于给虚构节点绑定hooks, 如果是组件类型,则递归绑定hooksfunction setTransitionHooks(vnode, hooks) { if (vnode.component) { setTransitionHooks(vnode.component.subTree, hooks); } else { vnode.transition = hooks; }}
通过下面的代码能够晓得,JavaScript
钩子函数,次要是在beforeEnter
、enter
、leave
阶段进行调用的。
接下来,实现过渡模式的实现。
过渡模式
过渡模式次要是为了解决多个元素之间的过渡成果,在不应用过渡模式的时候,元素之间过渡时,会被同时绘制。
这里是因为transition
组件默认进入和来到同时产生。
然而有时,咱们须要解决更简单的动作,比方须要使以后元素提前来到,实现之后再让新的元素进入等状况。
这就波及到元素组件间过渡状态的协调。
transition
组件为用于提供了两种模式:
out-in
: 以后元素先进行来到过渡,实现之后新元素过渡进入。in-out
: 新元素先进行进入过渡,实现之后以后元素过渡来到。
接下来就是获取以后元素与新元素,并在适合的机会执行对应的钩子就能够。
以out-in
为例,咱们心愿达到的成果是以后元素来到之后,在开始新元素的过渡。
咱们能够定义一个以后元素的来到钩子,在渲染其中,当须要移除 || 挪动以后元素的时候,咱们能够先执行以后元素的来到钩子,之后再调用新元素的进入钩子。
这就实现了out-in
的成果。
渲染器解决逻辑
咱们能够看下渲染器中是在哪个阶段解决的。
patch
阶段,通过move
函数来实现节点的插入。
// move & remove函数位于baseCreateRenderer函数中// 挪动节点const move = (vnode, container, anchor, moveType, parentSuspense = null) => { const { el, type, transition, children, shapeFlag } = vnode; // 省略局部代码... // single nodes const needTransition = transition; if (needTransition) { // 省略局部代码... const { leave, delayLeave, afterLeave } = transition; // hostInsert函数负责将el插入container const remove = () => hostInsert(el, container, anchor); // 由performLeave函数执行leave钩子 // leave 钩子会取负责元素的插入与afterLeave钩子的执行 const performLeave = () => { leave(el, () => { remove(); afterLeave && afterLeave(); // out-in模式下 }); }; if (delayLeave) { // 要害:delayLeave函数负责实现以后元素的插入和leave钩子的调用 // in-out模式下,执行delayLeave delayLeave(el, remove, performLeave); } else { // 要害:performLeave函数负责leave钩子的调用,最终通过leave函数实现以后元素的插入和afterLeave钩子的调用 performLeave(); } }};
unmount
阶段会执行remove
函数。remove
函数会将元素从父节点移除。
// 移除Vnodeconst remove = vnode => { const { type, el, anchor, transition } = vnode; // 省略局部代码... // hostRemove函数会将el从其父元素中移除 & afterLeave函数的调用 const performRemove = () => { hostRemove(el); if (transition && !transition.persisted && transition.afterLeave) { // out-in模式下 transition.afterLeave(); } }; if (vnode.shapeFlag & 1 && transition && !transition.persisted) { const { leave, delayLeave } = transition; // 对leave函数做层包裹,afterLeave钩子最终交给leave钩子调用 const performLeave = () => leave(el, performRemove); if (delayLeave) { // 要害:delayLeave函数负责实现以后元素的移除和leave & afterLeave钩子的调用 // in-out模式下执行in-out delayLeave(vnode.el, performRemove, performLeave); } else { // 要害:performLeave函数实现leave钩子的调用 performLeave(); } } else { // 要害:performRemove函数负责元素的移除和afterLeave钩子的执行 performRemove(); }};
下面的代码咱们只需关注标注的要害局部即可。
如果对于渲染器不是很理解,想全面了解下面的代码并不事实。
这里只需简略晓得:
transition
组件高度依赖于渲染器。对于增加过渡模式的元素,在动效钩子中会存在afterLeave
或者delayLeave
钩子,由afterLeave
钩子负责以后元素先来到的成果。delayLeave
钩子负责以后元素推延来到的成果。
新增过渡模式钩子
开始实现过渡模式:
const MyTransitionBase = { setup(props, { slots }) { // 获取以后组件实例 const instance = getCurrentInstance(); const state = useTransitionState(); return () => { const children = slots.default() // 获取用户配置的过渡模式 const { mode } = props; // 获取新元素 const child = children[0]; // 解析新元素的hooks const enterHooks = resolveTransitionHooks(child, rawProps, state, instance) setTransitionHooks(child, enterHooks); // 获取以后元素 const oldChild = instance.subTree; // 解决过渡模式 if (oldChild && (!isSameVNodeType(child, oldChild))) { // 从以后元素解析动效钩子 const leavingHooks = resolveTransitionHooks(oldChild, rawProps, state, instance); // 为以后(旧)元素更新动效钩子 setTransitionHooks(oldChild, leavingHooks); if (mode === 'out-in') { // 为以后(旧)元素新增afterLeave钩子,afterLeave的执行会使以后实例触发updateEffect,进入更新阶段 leavingHooks.afterLeave = () => { instance.update(); }; } else if (mode === 'in-out') { // 为以后元素新增delayLeave钩子,delayLeave钩子会推延以后元素的来到动效 // earlyRemove && delayedLeave 回调由渲染器传入 // earlyRemove负责元素的挪动或者移除 // delayedLeave负责leave钩子的调用 leavingHooks.delayLeave = (el, earlyRemove, delayedLeave) => { // 获取缓存 const leavingVNodesCache = getLeavingNodesForType(state, oldChild); // 更新缓存 leavingVNodesCache[String(oldChild.key)] = oldChild; // 为以后元素定义一个公有leave回调 el._leaveCb = () => { earlyRemove(); el._leaveCb = undefined; delete enterHooks.delayedLeave; }; // 在新元素上绑定delayedLeave钩子,用于推延以后元素的离场动效 enterHooks.delayedLeave = delayedLeave; }; } } return child; }; }}
从下面的代码能够晓得,咱们通过getCurrentInstance
获取以后组件实例,从以后实例获取须要进行离场解决的以后元素。
当新元素与以后元素是不同类型时,进行过渡模式的解决:
out-in
模式下,为以后元素新增afterLeave
钩子。afterLeave
钩子通过手动调动update
,最终的触发机会由patch
逻辑决定或者作为leave
钩子函数的回调函数,在以后元素还没有卸载时,也就是state.isUnmounting = true
时执行。以后元素先实现离场过渡之后,新元素再开始入场过渡。in-out
模式下,为以后元素新增delayLeave
钩子。其实是一个推延执行的leave
钩子,回调earlyRemove
,delayedLeave
回调由渲染器传入。earlyRemove
负责节点的挪动或者删除操作,delayedLeave
是一个推延的leave
钩子函数。会在新元素入场前执行。
更改useTransitionState
函数
function useTransitionState { const state = { isMounted: false, isUnmounting: false, leavingVNodes: new Map() // 负责缓存以后(旧)元素vnode } // 省略局部代码...}// 负责获取缓存的旧vnodefunction getLeavingNodesForType(state, vnode) { const { leavingVNodes } = state; let leavingVNodesCache = leavingVNodes.get(vnode.type); if (!leavingVNodesCache) { leavingVNodesCache = Object.create(null); leavingVNodes.set(vnode.type, leavingVNodesCache); } return leavingVNodesCache;}
从下面代码可知,在state
中新增了leavingVNodes
,用于记录须要进行离场过渡的Vnode
。getLeavingNodesForType
函数则是依据以后元素类型获取Vnode
缓存。
更改resolveTransitionHooks
钩子,
function resolveTransitionHooks(vnode, props, state, instance) { // 省略局部代码... const key = String(vnode.key); const leavingVNodesCache = getLeavingNodesForType(state, vnode); const callHook = (hook, args) => { hook && hook(...args) }; const hooks = { mode, persisted, beforeEnter(el) { let hook = onBeforeEnter // 省略局部代码... // 解决v-show if (el._leaveCb) { el._leaveCb(true) } // 解决具备形同key的Vnode在应用v-if的状况 const leavingVNode = leavingVNodesCache[key] if ( leavingVNode && isSameVNodeType(vnode, leavingVNode) && leavingVNode.el!._leaveCb ) { leavingVNode.el!._leaveCb() } callHook(hook, [el]) }, leave(el, remove) { // 省略局部代码... const key = String(vnode.key); // remove回调由渲染器传入 // 会触发元素的挪动或者移除,并执行afterLeave钩子 if (state.isUnmounting) { return remove(); } callHook(onBeforeLeave, [el]); // 记录以后元素的Vnode leavingVNodesCache[key] = vnode; } }; return hooks;}
至此,咱们曾经实现了MyTransition
组件从class
反对到javacsript
钩子反对,再到过渡模式的反对工作。
总结
通过本文,咱们根本实现了一个demo
版的Transition
组件。MyTransition
组件绝对于Vue
内置Transition
组件还有很多不足之处,Transition
组件还做了很多更粗疏的解决,如被KeepAlive
包裹的组件的动效解决、应用v-show
或者v-if
进行切换的组件动效解决等。
但MyTransition
组件曾经能够很好的帮咱们理解Transition
组件的要害逻辑:
- 通过在不同的渲染阶段为组件增加动效
class
实质是通过渲染器在渲染过程中执行对应的钩子函数实现的。 - 利用嵌套的
requestAnimationFrame
实现在下一帧中增加对应动效class
。 - 在
leave
阶段,通过监听transitionend
||animationend
事件,移除动效class
。 - 通过
setTimeout
操作动效的继续成果。 - 通过拆分
Transition
与BaseTransition
组件,做css
动效与JavaScript
动效兼容。 - 尽管供内部应用的
JavaScript
钩子很多,但在在BaseTransition
组件外部,也就三个:beforeEnter
、enter
、leave
,其余的钩子通过逻辑判断被整合到这三个次要的钩子中。 - 过渡模式的重要之处在于获取以后元素与新元素,当是
out-in
时为以后元素增加afterLeave
钩子,in-out
时,为以后元素增加delayLeave
钩子,新元素增加delayedLeave
钩子。