transition是Vue提供的内置组件,用于定义单个元素或组件的过渡效果。transition组件只会把过渡效果应用到其包裹的内容上,而不会额外渲染 DOM 元素,也不会出现在可被检查的组件层级中。
从描述说明上看transition内置组件跟keep-alive组件的一些渲染效果是相同的:
不会渲染额外DOM元素,也不会出现在可被检查的组件层级中
本文主要想了解下transition一些实现上的细节,其中就包含与keep-alive同用时的相关处理,当然还有其他相关的细节。
transition是抽象组件,其特点有:
这部分逻辑实际上跟keep-alive(keep-alive细节文章)是相同的,当然还是有一些不同的逻辑。
var Transition = {
name: 'transition',
props: transitionProps,
abstract: true,
render: function render (h) {
// 相关逻辑
}
};
由abstract属性可知transition是抽象组件,这里主要还是需要梳理render函数的逻辑,这是渲染的核心逻辑。
从transition render函数的处理逻辑中可知:除了一些校验判断:是否存在子元素、单个元素、是否存在transition嵌套外,重要的逻辑有2点:
该函数的的主要逻辑就是获取真实子节点,具体逻辑如下:
function getRealChild (vnode) {
var compOptions = vnode && vnode.componentOptions;
if (compOptions && compOptions.Ctor.options.abstract) {
return getRealChild(getFirstComponentChild(compOptions.children))
} else {
return vnode
}
}
从上面逻辑中可以得知是获取逻辑:
获取第一个非抽象组件或者非组件的虚拟节点对象
从这里的逻辑可知:transition包含keep-alive为什么是有效的了。
if (
oldChild &&
oldChild.data &&
!isSameChild(child, oldChild) &&
!isAsyncPlaceholder(oldChild) &&
// #6687 component root is a comment node
!(oldChild.componentInstance && oldChild.componentInstance._vnode.isComment)
) {
if (mode === 'in-out') {
// 相关处理
} else if (mode === 'out-in') {
// 相关处理
}
}
当切换后之后针对不同mode模式的旧节点的处理逻辑,这里需要注意的是前置条件isSameChild即不是相同子节点,具体逻辑如下:
function isSameChild (child, oldChild) {
return oldChild.key === child.key && oldChild.tag === child.tag
}
从上面可知逻辑:新旧key值相同并且是相同类型tag,这里逻辑涉及到一个Vue transition的细节点:
当有相同标签名的元素切换时,需要通过 key attribute 设置唯一的值来标记以让 Vue 区分它们,否则 Vue 为了效率只会替换相同标签内部的内容。即使在技术上没有必要,给在 组件中的多个元素设置 key 是一个更好的实践
对于相同标签名切换只会替换内容,而不会应用过渡效果。为什么Vue针对相同标签名需要设置key来区分才会存在动画效果?
实际上这边涉及到diff算法,diff算法尽可能复用节点,而对于相同标签名的元素不通内容的元素其diff算法的处理逻辑仅仅就是替换textContent
Vue在插入、更新或移除DOM时,提供多种不同方式的应用过渡效果,实际上具体分为2类:
这里通过css方式的实例来梳理相关逻辑,实例如下:
<transition name="fade">
<div v-if="isShow">测试1div>
<span v-else>测试2span>
transition>
Vue transition通过css实现过渡效果,实际上是通过应用对应的className来实现过渡效果。通过debug源码可知上面实例效果引用是通过patch阶段的invokeCreateHooks调用的。
function invokeCreateHooks (vnode, insertedVnodeQueue) {
for (var i$1 = 0; i$1 < cbs.create.length; ++i$1) {
cbs.create[i$1](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相关方法,这里需要注意的是cbs相关内容。相关逻辑如下:
var modules = [
attrs,
klass,
events,
domProps,
style,
transition,
ref,
directives
];
var patch = createPatchFunction({ nodeOps: nodeOps, modules: modules });
实际上modules中每一个都定义了一些hook函数,例如transition如下:
var transition = inBrowser ? {
create: _enter,
activate: _enter,
remove: function remove$$1 (vnode, rm) {
/* istanbul ignore else */
if (vnode.data.show !== true) {
leave(vnode, rm);
} else {
rm();
}
}
} : {};
而cbs对象就是所有modules中相应hook的集合,实际上cbs存在如下属性:
var hooks = ['create', 'activate', 'update', 'remove', 'destroy'];
for (i = 0; i < hooks.length; ++i) {
cbs[hooks[i]] = [];
for (j = 0; j < modules.length; ++j) {
if (isDef(modules[j][hooks[i]])) {
cbs[hooks[i]].push(modules[j][hooks[i]]);
}
}
}
实际上cbs对应的modules都是与相应属性集合相关即一个属性类型对应一个module对象(attrs、domProps、style等),这里主要关注的是transition的create相关处理即_enter函数:
function _enter (_, vnode) {
if (vnode.data.show !== true) {
enter(vnode);
}
}
这里需要注意的是show条件的处理,transition可以给任何元素和组件添加进入/离开过渡,具体条件:
而对于transition下存在v-show指令的处理是特殊的,实际上过渡效果逻辑是在v-show指令实现中处理的,Vue源码中使用data.show表示是否存在v-show指令的。对于非v-show的处理还是通过transition module对象的相关hook的处理逻辑:
实际上对于transition进入/离开一些处理逻辑,Vue官网提供了相关信息(Vue官网进入/离开&列表过渡文章),这些信息对于去研究源码中enter和leave函数逻辑是非常有帮助的。
enter函数处理进入阶段的动画逻辑,该函数主要逻辑如下:
上面是enter函数的一些主要处理逻辑,实际上核心逻辑有3处:
该函数主要功能是依据组件的内部属性transition来生成相关css类。实际上就是依据name属性去生成对应的enterClass、enterToClass、enterActiveClass、leaveClass、leaveToClass、leaveActiveClass。
首先看看这2个变量的定义:
// css变量表示是否使用 CSS 过渡类,默认是true
var expectsCSS = css !== false && !isIE9;
// 判断enterHook参数个数
var userWantsControl = getHookArgumentsLength(enterHook);
expectsCSS实际上就是是否使用css过渡类的标识,如果仅使用JavaScript过渡的话css值建议设置false。
userWantsControl是判断enter钩子函数的参数个数:
// 当与 CSS 结合使用时回调函数 done 是可选的
enter: function (el, done) {
// ...
done()
}
实际上getHookArgumentsLength是判断参数个数是否 > 1即处理上面场景的,这里先不关注这边相关逻辑。
实际上这2个变量是非常重要,它们涉及到一些比较重要的逻辑
if (expectsCSS) {
addTransitionClass(el, startClass);
addTransitionClass(el, activeClass);
nextFrame(function () {
removeTransitionClass(el, startClass);
if (!cb.cancelled) {
addTransitionClass(el, toClass);
if (!userWantsControl) {
if (isValidDuration(explicitEnterDuration)) {
setTimeout(cb, explicitEnterDuration);
} else {
whenTransitionEnds(el, type, cb);
}
}
}
});
}
当可以应用css过渡类时,实际上enter函数中主要逻辑之一就是上面的处理,主要逻辑说明如下:
在看whenTransitionEnds的逻辑之前,稍微了解下enterCb回调函数逻辑具体如下:
var cb = el._enterCb = once(function () {
// 删除toClass、activeClass
if (expectsCSS) {
removeTransitionClass(el, toClass);
removeTransitionClass(el, activeClass);
}
// enterCancelled相关处理
if (cb.cancelled) {
if (expectsCSS) {
removeTransitionClass(el, startClass);
}
enterCancelledHook && enterCancelledHook(el);
} else {
afterEnterHook && afterEnterHook(el);
}
el._enterCb = null;
});
实际上主要移除toClass、activeClass,即enter阶段执行完成了。
该函数的核心逻辑是使用addEventListener添加transitionEnd或animateEnd事件,Vue transition提供了type属性来指定过渡类型侦听过渡何时结束,如果不提供type值默认 Vue.js 将自动检测出持续时间长的为过渡事件类型(实际上是就是获取css对应的transition-duration或animate-duration的值来判断哪个值最大就是对应的type的值)。
使用css类过渡时,其enter函数的处理逻辑可以简述为如下步骤:
leave处理离开阶段动画的处理逻辑,其触发逻辑如下:
remove: function remove$$1 (vnode, rm) {
/* istanbul ignore else */
if (vnode.data.show !== true) {
leave(vnode, rm);
} else {
rm();
}
}
这里逻辑区分了v-show的情况,对于非v-show直接调用了leave函数。实际上的处理逻辑与enter函数基本类型,不同点在于下面2点:
实际上对于transition下存在v-show是需要特殊处理的,实际上的特殊体现在2个方面:
指令相关细节本文旧不再具体讨论,这里看下v-show指令与transition leave一些逻辑,如下:
if (transition$$1) {
vnode.data.show = true;
if (value) {
enter(vnode, function () {
el.style.display = el.__vOriginalDisplay;
});
} else {
leave(vnode, function () {
el.style.display = 'none';
});
}
}
leave函数第2个参数是一个函数就是leave中用于去除旧节点的逻辑。
本文实际上是以css过渡实例来梳理transition相关逻辑的,一些点梳理相对有限还存在许多不足例如JavaScript钩子函数、css动画、其他一些场景的细节处理。但基本上一些点从源码层次上去加深了理解,主要有:
内部组件transition的处理逻辑相来说比较复杂,建议debug去具体分析可以加深对其的理解。