Vue源码之transition细节

前言

transition是Vue提供的内置组件,用于定义单个元素或组件的过渡效果。transition组件只会把过渡效果应用到其包裹的内容上,而不会额外渲染 DOM 元素,也不会出现在可被检查的组件层级中。
从描述说明上看transition内置组件跟keep-alive组件的一些渲染效果是相同的:

不会渲染额外DOM元素,也不会出现在可被检查的组件层级中

本文主要想了解下transition一些实现上的细节,其中就包含与keep-alive同用时的相关处理,当然还有其他相关的细节。

transition组件定义

transition是抽象组件,其特点有:

  • 自身不会渲染一个DOM元素
  • 不会出现在组件的父组件链中

这部分逻辑实际上跟keep-alive(keep-alive细节文章)是相同的,当然还是有一些不同的逻辑。

    var Transition = {
      name: 'transition',
      props: transitionProps,
      abstract: true,
  
      render: function render (h) {
      	// 相关逻辑
      }
    };

由abstract属性可知transition是抽象组件,这里主要还是需要梳理render函数的逻辑,这是渲染的核心逻辑。

在这里插入图片描述
从transition render函数的处理逻辑中可知:除了一些校验判断:是否存在子元素、单个元素、是否存在transition嵌套外,重要的逻辑有2点:

  • getRealChild函数
  • 旧节点的相关处理逻辑
getRealChild函数

该函数的的主要逻辑就是获取真实子节点,具体逻辑如下:

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

transition过渡效果处理逻辑

Vue在插入、更新或移除DOM时,提供多种不同方式的应用过渡效果,实际上具体分为2类:

  • 使用css来应用过渡效果
  • 使用钩子函数来应用过渡效果

这里通过css方式的实例来梳理相关逻辑,实例如下:

<transition name="fade">
	<div v-if="isShow">测试1</div>
	<span v-else>测试2</span>
</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可以给任何元素和组件添加进入/离开过渡,具体条件:

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

而对于transition下存在v-show指令的处理是特殊的,实际上过渡效果逻辑是在v-show指令实现中处理的,Vue源码中使用data.show表示是否存在v-show指令的。对于非v-show的处理还是通过transition module对象的相关hook的处理逻辑:

  • create hook定义创建阶段,对应调用enter函数来做进入动画相关处理
  • remove hook定义移除阶段,对应调用leave函数来做离开动画相关处理

实际上对于transition进入/离开一些处理逻辑,Vue官网提供了相关信息(Vue官网进入/离开&列表过渡文章),这些信息对于去研究源码中enter和leave函数逻辑是非常有帮助的。

enter函数处理逻辑

enter函数处理进入阶段的动画逻辑,该函数主要逻辑如下:
在这里插入图片描述
上面是enter函数的一些主要处理逻辑,实际上核心逻辑有3处:

  • resolveTransition函数生成data
  • 关于css嗅探和JavaScript执行逻辑的区分以及相关逻辑,需要依据expectsCSS和userWantsControl这2个属性
  • enterCb函数的具体逻辑
resolveTransition函数具体逻辑

该函数主要功能是依据组件的内部属性transition来生成相关css类。实际上就是依据name属性去生成对应的enterClass、enterToClass、enterActiveClass、leaveClass、leaveToClass、leaveActiveClass。

变量expectsCSS和userWantsControl的意义

首先看看这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函数中主要逻辑之一就是上面的处理,主要逻辑说明如下:

  • 添加startClass、activeClass对应的className到dom上
  • nextFrame实际上就是调用requestAnimationFrame 或 setTimeout下实现下一帧渲染
  • whenTransitionEnds函数调用:实际上注册对应的transitionend或animationend事件执行enterCb回调函数

在看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阶段执行完成了。

whenTransitionEnds函数逻辑

该函数的核心逻辑是使用addEventListener添加transitionEnd或animateEnd事件,Vue transition提供了type属性来指定过渡类型侦听过渡何时结束,如果不提供type值默认 Vue.js 将自动检测出持续时间长的为过渡事件类型(实际上是就是获取css对应的transition-duration或animate-duration的值来判断哪个值最大就是对应的type的值)。

总结

使用css类过渡时,其enter函数的处理逻辑可以简述为如下步骤:

  1. 获取相关props,通过name属性获取对应的6个class类
  2. 添加startClass即fade-enter
  3. 添加activeClass即fade-enter-active
  4. 使用requestAnimationFrame或setTimeOut在下一帧执行相关逻辑:添加toClass即fade-enter-to,移除startClass即fade-enter
  5. 在合法duration条件下,通过动态注册transitionend或animationend来侦听过渡结束后移除enter相关class即移除fade-enter-to、fade-enter-active
leave函数处理

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点:

  • 相比enter函数,leave函数处理是leave相关css类,具体是(通过css实例即name为fade):fade-leave、fade-leave-active、fade-leave-to
  • 相对于enterCb,leaveCb回调函数除了移除相关css类,会执行删除旧dom节点的操作

transition下存在v-show组件的特殊处理

实际上对于transition下存在v-show是需要特殊处理的,实际上的特殊体现在2个方面:

  • 此情况下的enter、leave触发处理是通过v-show指令的bind、update处理的而非patch阶段的invokeCreateHook、removeAndInvokeRemoveHook
  • 此情况下的leave函数中不会删除旧节点dom,而是设置css 对应元素 display:none来处理旧节点

指令相关细节本文旧不再具体讨论,这里看下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执行的整体的大概流程
  • 一些transition的细节点,具体有:
    • v-show与其他情形过渡区别
    • enter阶段和leave阶段的class类的处理流程
    • 过渡事件类型背后transitionend和animationend选择的逻辑
    • transition与keep-alive共用的逻辑处理

内部组件transition的处理逻辑相来说比较复杂,建议debug去具体分析可以加深对其的理解。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值