transition组件大家肯定都用过,这个组件是Vue 在插入、更新或者移除 DOM 时,提供多种不同方式的应用过渡效果,现在我们从源码看看vue是怎么实现的。
只有css
// app.vue
<template>
<div>
<button v-on:click="show = !show">Toggle</button>
<transition name="fade">
<p v-if="show">hello</p>
</transition>
</div>
</template>
<script>
export default {
name: 'app',
data() {
return {
show: true,
};
},
};
</script>
<style>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.65s;
}
.fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ {
opacity: 0;
}
</style>
...
var Transition = {
name: 'transition',
props: transitionProps,
abstract: true,
render: function (h) {
var _this = this;
var children = this.$slots.default;
if (!children) {
return;
}
// filter out text nodes (possible whitespaces)
children = children.filter(isNotTextNode);
/* istanbul ignore if */
if (!children.length) {
return;
}
// warn multiple elements
if (children.length > 1) {
warn$2('<transition> can only be used on a single element. Use ' +
'<transition-group> for lists.', this.$parent);
}
var mode = this.mode;
// warn invalid mode
if (mode && mode !== 'in-out' && mode !== 'out-in') {
warn$2('invalid <transition> mode: ' + mode, this.$parent);
}
var rawChild = children[0];
// if this is a component root node and the component's
// parent container node also has transition, skip.
if (hasParentTransition(this.$vnode)) {
return rawChild;
}
// apply transition data to child
// use getRealChild() to ignore abstract components e.g. keep-alive
var child = getRealChild(rawChild);
/* istanbul ignore if */
if (!child) {
return rawChild;
}
if (this._leaving) {
return placeholder(h, rawChild);
}
// ensure a key that is unique to the vnode type and to this transition
// component instance. This key will be used to remove pending leaving nodes
// during entering.
var id = "__transition-".concat(this._uid, "-");
child.key =
child.key == null
? child.isComment
? id + 'comment'
: id + child.tag
: isPrimitive(child.key)
? String(child.key).indexOf(id) === 0
? child.key
: id + child.key
: child.key;
var data = ((child.data || (child.data = {})).transition =
extractTransitionData(this));
var oldRawChild = this._vnode;
var oldChild = getRealChild(oldRawChild);
// mark v-show
// so that the transition module can hand over the control to the directive
if (child.data.directives && child.data.directives.some(isVShowDirective)) {
child.data.show = true;
}
if (oldChild &&
oldChild.data &&
!isSameChild(child, oldChild) &&
!isAsyncPlaceholder(oldChild) &&
// #6687 component root is a comment node
!(oldChild.componentInstance &&
oldChild.componentInstance._vnode.isComment)) {
// replace old child transition data with fresh one
// important for dynamic transitions!
var oldData = (oldChild.data.transition = extend({}, data));
// handle transition mode
if (mode === 'out-in') {
// return placeholder node and queue update when leave finishes
this._leaving = true;
mergeVNodeHook(oldData, 'afterLeave', function () {
_this._leaving = false;
_this.$forceUpdate();
});
return placeholder(h, rawChild);
}
else if (mode === 'in-out') {
if (isAsyncPlaceholder(child)) {
return oldRawChild;
}
var delayedLeave_1;
var performLeave = function () {
delayedLeave_1();
};
mergeVNodeHook(data, 'afterEnter', performLeave);
mergeVNodeHook(data, 'enterCancelled', performLeave);
mergeVNodeHook(oldData, 'delayLeave', function (leave) {
delayedLeave_1 = leave;
});
}
}
return rawChild;
}
};
transition也是vue的一个内置组件,渲染函数如上所述,最后会返回rawChild,这个是$slots.default中的第一个vnode。这就意味着如果你在里面写入超过一个元素,也就只有第一个有动画效果。当我们点击toggle的时候,hello会消失,此时会触发removeVnodes([oldVnode])方法:
function removeVnodes(vnodes, startIdx, endIdx) {
for (; startIdx <= endIdx; ++startIdx) {
var ch = vnodes[startIdx];
if (isDef(ch)) {
if (isDef(ch.tag)) {
removeAndInvokeRemoveHook(ch);
invokeDestroyHook(ch);
}
else {
// Text node
removeNode(ch.elm);
}
}
}
}
该方法主要执行removeAndInvokeRemoveHook(ch):
function removeAndInvokeRemoveHook(vnode, rm) {
if (isDef(rm) || isDef(vnode.data)) {
var i_3;
var listeners = cbs.remove.length + 1;
if (isDef(rm)) {
// we have a recursively passed down rm callback
// increase the listeners count
rm.listeners += listeners;
}
else {
// directly removing
rm = createRmCb(vnode.elm, listeners);
}
// recursively invoke hooks on child component root node
if (isDef((i_3 = vnode.componentInstance)) &&
isDef((i_3 = i_3._vnode)) &&
isDef(i_3.data)) {
removeAndInvokeRemoveHook(i_3, rm);
}
for (i_3 = 0; i_3 < cbs.remove.length; ++i_3) {
cbs.remove[i_3](vnode, rm);
}
if (isDef((i_3 = vnode.data.hook)) && isDef((i_3 = i_3.remove))) {
i_3(vnode, rm);
}
else {
rm();
}
}
else {
removeNode(vnode.elm);
}
}
主要执行cbs.remove[i_3](vnode, rm)的leave(vnode, rm)方法:
function leave(vnode, rm) {
var el = vnode.elm;
// call enter callback now
if (isDef(el._enterCb)) {
el._enterCb.cancelled = true;
el._enterCb();
}
var data = resolveTransition(vnode.data.transition);
if (isUndef(data) || el.nodeType !== 1) {
return rm();
}
/* istanbul ignore if */
if (isDef(el._leaveCb)) {
return;
}
var css = data.css, type = data.type, leaveClass = data.leaveClass, leaveToClass = data.leaveToClass, leaveActiveClass = data.leaveActiveClass, beforeLeave = data.beforeLeave, leave = data.leave, afterLeave = data.afterLeave, leaveCancelled = data.leaveCancelled, delayLeave = data.delayLeave, duration = data.duration;
var expectsCSS = css !== false && !isIE9;
var userWantsControl = getHookArgumentsLength(leave);
var explicitLeaveDuration = toNumber(isObject(duration) ? duration.leave : duration);
if (isDef(explicitLeaveDuration)) {
checkDuration(explicitLeaveDuration, 'leave', vnode);
}
var cb = (el._leaveCb = once(function () {
if (el.parentNode && el.parentNode._pending) {
el.parentNode._pending[vnode.key] = null;
}
if (expectsCSS) {
removeTransitionClass(el, leaveToClass);
removeTransitionClass(el, leaveActiveClass);
}
// @ts-expect-error
if (cb.cancelled) {
if (expectsCSS) {
removeTransitionClass(el, leaveClass);
}
leaveCancelled && leaveCancelled(el);
}
else {
rm();
afterLeave && afterLeave(el);
}
el._leaveCb = null;
}));
if (delayLeave) {
delayLeave(performLeave);
}
else {
performLeave();
}
function performLeave() {
// the delayed leave may have already been cancelled
// @ts-expect-error
if (cb.cancelled) {
return;
}
// record leaving element
if (!vnode.data.show && el.parentNode) {
(el.parentNode._pending || (el.parentNode._pending = {}))[vnode.key] =
vnode;
}
beforeLeave && beforeLeave(el);
if (expectsCSS) {
addTransitionClass(el, leaveClass);
addTransitionClass(el, leaveActiveClass);
nextFrame(function () {
removeTransitionClass(el, leaveClass);
// @ts-expect-error
if (!cb.cancelled) {
addTransitionClass(el, leaveToClass);
if (!userWantsControl) {
if (isValidDuration(explicitLeaveDuration)) {
setTimeout(cb, explicitLeaveDuration);
}
else {
whenTransitionEnds(el, type, cb);
}
}
}
});
}
leave && leave(el, cb);
if (!expectsCSS && !userWantsControl) {
cb();
}
}
}
该方法首先执行resolveTransition(vnode.data.transition)会根据名称拼凑动画离开和进入的类。
然后执行performLeave方法:
function performLeave() {
// the delayed leave may have already been cancelled
// @ts-expect-error
if (cb.cancelled) {
return;
}
// record leaving element
if (!vnode.data.show && el.parentNode) {
(el.parentNode._pending || (el.parentNode._pending = {}))[vnode.key] =
vnode;
}
beforeLeave && beforeLeave(el);
if (expectsCSS) {
addTransitionClass(el, leaveClass);
addTransitionClass(el, leaveActiveClass);
nextFrame(function () {
removeTransitionClass(el, leaveClass);
// @ts-expect-error
if (!cb.cancelled) {
addTransitionClass(el, leaveToClass);
if (!userWantsControl) {
if (isValidDuration(explicitLeaveDuration)) {
setTimeout(cb, explicitLeaveDuration);
}
else {
whenTransitionEnds(el, type, cb);
}
}
}
});
}
leave && leave(el, cb);
if (!expectsCSS && !userWantsControl) {
cb();
}
}
}
首先执行addTransitionClass(el, leaveClass)新增离开时的类,然后执行addTransitionClass(el, leaveActiveClass)新增离开中的类,随后执行nextFrame方法。该方法是requestAnimationFrame函数,首先执行removeTransitionClass(el, leaveClass)移除离开时的类,再执行addTransitionClass(el, leaveToClass)添加离开完成的类。然后执行whenTransitionEnds(el, type, cb)方法:
function whenTransitionEnds(el, expectedType, cb) {
var _a = getTransitionInfo(el, expectedType), type = _a.type, timeout = _a.timeout, propCount = _a.propCount;
if (!type)
return cb();
var event = type === TRANSITION ? transitionEndEvent : animationEndEvent;
var ended = 0;
var end = function () {
el.removeEventListener(event, onEnd);
cb();
};
var onEnd = function (e) {
if (e.target === el) {
if (++ended >= propCount) {
end();
}
}
};
setTimeout(function () {
if (ended < propCount) {
end();
}
}, timeout + 1);
el.addEventListener(event, onEnd);
}
首先执行getTransitionInfo去获取当前的transition时间,然后监听transitionEnd事件。等待动画执行完毕移除监听事件,然后执行cb()方法,该方法是:
var cb = (el._leaveCb = once(function () {
if (el.parentNode && el.parentNode._pending) {
el.parentNode._pending[vnode.key] = null;
}
if (expectsCSS) {
removeTransitionClass(el, leaveToClass);
removeTransitionClass(el, leaveActiveClass);
}
// @ts-expect-error
if (cb.cancelled) {
if (expectsCSS) {
removeTransitionClass(el, leaveClass);
}
leaveCancelled && leaveCancelled(el);
}
else {
rm();
afterLeave && afterLeave(el);
}
el._leaveCb = null;
}));
执行removeTransitionClass(el, leaveToClass)和removeTransitionClass(el, leaveActiveClass)移除执行完的类后执行rm()方法的removeNode(childElm)方法去移除子元素。到此动画执行完毕。
function remove() {
if (--remove.listeners === 0) {
removeNode(childElm);
}
}
function removeNode(el) {
var parent = nodeOps.parentNode(el);
// element may have already been removed due to v-html / v-text
if (isDef(parent)) {
nodeOps.removeChild(parent, el);
}
}
小结
在移除元素的时候会执行removeAndInvokeRemoveHook方法去给要隐藏的元素添加离开的类并在下一帧中移除移除离开起始的类,随后获取元素设置的transition时间,并启用setTimeout去等待动画执行完成。执行完后会执行一个回调函数去移除当前dom上的类,并通过父元素移除当前子元素。
函数式
<template>
<div>
<button @click="show = !show">Toggle</button>
<transition
@before-enter="beforeEnter"
@enter="enter"
@leave="leave"
@css="false"
>
<p v-if="show">Demo</p>
</transition>
</div>
</template>
<script>
export default {
name: 'app',
data() {
return {
show: true,
};
},
methods: {
beforeEnter: function (el) {
console.log(1234);
el.style.opacity = 0;
el.style.transformOrigin = 'left';
},
enter: function (el, done) {
Velocity(el, { opacity: 1, fontSize: '1.4em' }, { duration: 300 });
Velocity(el, { fontSize: '1em' }, { complete: done });
},
leave: function (el, done) {
console.log(7758);
Velocity(el, { translateX: '15px', rotateZ: '50deg' }, { duration: 600 });
Velocity(el, { rotateZ: '100deg' }, { loop: 2 });
Velocity(
el,
{
rotateZ: '45deg',
translateY: '30px',
translateX: '30px',
opacity: 0,
},
{ complete: done }
);
},
},
};
</script>
可以看到我们定义了三个函数,一个是进入前和进入中、离开中三个函数。然后我们再来看看此时transition的渲染函数:
var Transition = {
name: 'transition',
props: transitionProps,
abstract: true,
render: function (h) {
var _this = this;
var children = this.$slots.default;
if (!children) {
return;
}
// filter out text nodes (possible whitespaces)
children = children.filter(isNotTextNode);
/* istanbul ignore if */
if (!children.length) {
return;
}
// warn multiple elements
if (children.length > 1) {
warn$2('<transition> can only be used on a single element. Use ' +
'<transition-group> for lists.', this.$parent);
}
var mode = this.mode;
// warn invalid mode
...
var rawChild = children[0];
// if this is a component root node and the component's
// parent container node also has transition, skip.
if (hasParentTransition(this.$vnode)) {
return rawChild;
}
// apply transition data to child
// use getRealChild() to ignore abstract components e.g. keep-alive
var child = getRealChild(rawChild);
/* istanbul ignore if */
if (!child) {
return rawChild;
}
if (this._leaving) {
return placeholder(h, rawChild);
}
// ensure a key that is unique to the vnode type and to this transition
// component instance. This key will be used to remove pending leaving nodes
// during entering.
var id = "__transition-".concat(this._uid, "-");
child.key =
child.key == null
? child.isComment
? id + 'comment'
: id + child.tag
: isPrimitive(child.key)
? String(child.key).indexOf(id) === 0
? child.key
: id + child.key
: child.key;
var data = ((child.data || (child.data = {})).transition =
extractTransitionData(this));
var oldRawChild = this._vnode;
var oldChild = getRealChild(oldRawChild);
// mark v-show
// so that the transition module can hand over the control to the directive
if (child.data.directives && child.data.directives.some(isVShowDirective)) {
child.data.show = true;
}
...
return rawChild;
}
};
我们重点看var data = ((child.data || (child.data = {})).transition =
extractTransitionData(this)),这个是获取transition的_parentListeners并赋给p元素的vnode:
最后返回p的vnode。我们先来看离开时是啥时候触发的。离开执行的是 removeVnodes([oldVnode], 0, 0),该函数主要执行 removeAndInvokeRemoveHook(ch)的cbs.remove[i_3](vnode, rm)方法:
var transition = inBrowser
? {
...
remove: function (vnode, rm) {
/* istanbul ignore else */
if (vnode.data.show !== true) {
// @ts-expect-error
leave(vnode, rm);
}
else {
rm();
}
}
}
: {};
可以看出执行的是leave(vnode, rm)方法:
function leave(vnode, rm) {
var el = vnode.elm;
// call enter callback now
if (isDef(el._enterCb)) {
el._enterCb.cancelled = true;
el._enterCb();
}
// 是否有transition
var data = resolveTransition(vnode.data.transition);
if (isUndef(data) || el.nodeType !== 1) {
return rm();
}
/* istanbul ignore if */
if (isDef(el._leaveCb)) {
return;
}
var css = data.css, type = data.type, leaveClass = data.leaveClass, leaveToClass = data.leaveToClass, leaveActiveClass = data.leaveActiveClass, beforeLeave = data.beforeLeave, leave = data.leave, afterLeave = data.afterLeave, leaveCancelled = data.leaveCancelled, delayLeave = data.delayLeave, duration = data.duration;
var expectsCSS = css !== false && !isIE9;
var userWantsControl = getHookArgumentsLength(leave);
var explicitLeaveDuration = toNumber(isObject(duration) ? duration.leave : duration);
if (isDef(explicitLeaveDuration)) {
checkDuration(explicitLeaveDuration, 'leave', vnode);
}
var cb = (el._leaveCb = once(function () {
if (el.parentNode && el.parentNode._pending) {
el.parentNode._pending[vnode.key] = null;
}
if (expectsCSS) {
removeTransitionClass(el, leaveToClass);
removeTransitionClass(el, leaveActiveClass);
}
// @ts-expect-error
if (cb.cancelled) {
if (expectsCSS) {
removeTransitionClass(el, leaveClass);
}
leaveCancelled && leaveCancelled(el);
}
else {
rm();
afterLeave && afterLeave(el);
}
el._leaveCb = null;
}));
if (delayLeave) {
delayLeave(performLeave);
}
else {
performLeave();
}
function performLeave() {
// the delayed leave may have already been cancelled
// @ts-expect-error
if (cb.cancelled) {
return;
}
// record leaving element
if (!vnode.data.show && el.parentNode) {
(el.parentNode._pending || (el.parentNode._pending = {}))[vnode.key] =
vnode;
}
beforeLeave && beforeLeave(el);
if (expectsCSS) {
addTransitionClass(el, leaveClass);
addTransitionClass(el, leaveActiveClass);
nextFrame(function () {
removeTransitionClass(el, leaveClass);
// @ts-expect-error
if (!cb.cancelled) {
addTransitionClass(el, leaveToClass);
if (!userWantsControl) {
if (isValidDuration(explicitLeaveDuration)) {
setTimeout(cb, explicitLeaveDuration);
}
else {
whenTransitionEnds(el, type, cb);
}
}
}
});
}
leave && leave(el, cb);
if (!expectsCSS && !userWantsControl) {
cb();
}
}
}
先判断是否有transition对象,如果有的话会执行如果有 beforeLeave(el)方法,然后判断是否需要css,我们已经表示不需要,我们自己去处理。然后执行leave(el, cb)函数触发我们设置的回调。随后执行完cbs.remove[i_3](vnode, rm)会去执行removeNode方法移除函数。
下面我们看看beforeEnter和enter的触发时机,它们执行的阶段是在createElm阶段,在执行insert(parentElm, vnode.elm, refElm)方法前会执行invokeCreateHooks方法去执行插入前的一个方法:
function invokeCreateHooks(vnode, insertedVnodeQueue) {
for (var i_2 = 0; i_2 < cbs.create.length; ++i_2) {
cbs.create[i_2](emptyNode, vnode);
}
i = vnode.data.hook; // Reuse variable
if (isDef(i)) {
if (isDef(i.create))
i.create(emptyNode, vnode);
if (isDef(i.insert))
insertedVnodeQueue.push(vnode);
}
}
通过遍历执行cbs.create[i_2](emptyNode, vnode)方法中的_enter(_, vnode)方法,去判断如果有vnode.data.beforeEnter就会执行回调。
function enter(vnode, toggleDisplay) {
var el = vnode.elm;
// call leave callback now
if (isDef(el._leaveCb)) {
el._leaveCb.cancelled = true;
el._leaveCb();
}
var data = resolveTransition(vnode.data.transition);
if (isUndef(data)) {
return;
}
/* istanbul ignore if */
if (isDef(el._enterCb) || el.nodeType !== 1) {
return;
}
var css = data.css, type = data.type, enterClass = data.enterClass, enterToClass = data.enterToClass, enterActiveClass = data.enterActiveClass, appearClass = data.appearClass, appearToClass = data.appearToClass, appearActiveClass = data.appearActiveClass, beforeEnter = data.beforeEnter, enter = data.enter, afterEnter = data.afterEnter, enterCancelled = data.enterCancelled, beforeAppear = data.beforeAppear, appear = data.appear, afterAppear = data.afterAppear, appearCancelled = data.appearCancelled, duration = data.duration;
// activeInstance will always be the <transition> component managing this
// transition. One edge case to check is when the <transition> is placed
// as the root node of a child component. In that case we need to check
// <transition>'s parent for appear check.
var context = activeInstance;
var transitionNode = activeInstance.$vnode;
while (transitionNode && transitionNode.parent) {
context = transitionNode.context;
transitionNode = transitionNode.parent;
}
var isAppear = !context._isMounted || !vnode.isRootInsert;
if (isAppear && !appear && appear !== '') {
return;
}
var startClass = isAppear && appearClass ? appearClass : enterClass;
var activeClass = isAppear && appearActiveClass ? appearActiveClass : enterActiveClass;
var toClass = isAppear && appearToClass ? appearToClass : enterToClass;
var beforeEnterHook = isAppear ? beforeAppear || beforeEnter : beforeEnter;
var enterHook = isAppear ? (isFunction(appear) ? appear : enter) : enter;
var afterEnterHook = isAppear ? afterAppear || afterEnter : afterEnter;
var enterCancelledHook = isAppear
? appearCancelled || enterCancelled
: enterCancelled;
var explicitEnterDuration = toNumber(isObject(duration) ? duration.enter : duration);
if (explicitEnterDuration != null) {
checkDuration(explicitEnterDuration, 'enter', vnode);
}
var expectsCSS = css !== false && !isIE9;
var userWantsControl = getHookArgumentsLength(enterHook);
var cb = (el._enterCb = once(function () {
if (expectsCSS) {
removeTransitionClass(el, toClass);
removeTransitionClass(el, activeClass);
}
// @ts-expect-error
if (cb.cancelled) {
if (expectsCSS) {
removeTransitionClass(el, startClass);
}
enterCancelledHook && enterCancelledHook(el);
}
else {
afterEnterHook && afterEnterHook(el);
}
el._enterCb = null;
}));
if (!vnode.data.show) {
// remove pending leave element on enter by injecting an insert hook
mergeVNodeHook(vnode, 'insert', function () {
var parent = el.parentNode;
var pendingNode = parent && parent._pending && parent._pending[vnode.key];
if (pendingNode &&
pendingNode.tag === vnode.tag &&
pendingNode.elm._leaveCb) {
pendingNode.elm._leaveCb();
}
enterHook && enterHook(el, cb);
});
}
// start enter transition
beforeEnterHook && beforeEnterHook(el);
if (expectsCSS) {
addTransitionClass(el, startClass);
addTransitionClass(el, activeClass);
nextFrame(function () {
removeTransitionClass(el, startClass);
// @ts-expect-error
if (!cb.cancelled) {
addTransitionClass(el, toClass);
if (!userWantsControl) {
if (isValidDuration(explicitEnterDuration)) {
setTimeout(cb, explicitEnterDuration);
}
else {
whenTransitionEnds(el, type, cb);
}
}
}
});
}
if (vnode.data.show) {
toggleDisplay && toggleDisplay();
enterHook && enterHook(el, cb);
}
if (!expectsCSS && !userWantsControl) {
cb();
}
}
该函数主要做了两件事,首先就是给当前vnode绑定了insert事件,然后触发beforeEnterHook方法。insert方法中我们可以看到会执行enterHook,那么insert方法什么时候触发呢?我们在前面的文章也分析过,insert的触发是在没有parentVnode的时候才会触发,此时会执行queue[i_6].data.hook.insert(queue[i_6])去最终执行enterHook方法。
小结
函数式的transition方法的beforeLeave和leave是在有 removeVnodes阶段执行的,enterHook是在最后insert时执行。