一个 DOM 节点的插入和删除或者是显示和隐藏,不想让它特别生硬,通常会考虑加一些过渡效果
Vue.js 除了实现了强大的数据驱动,组件化的能力,也提供了一整套过渡的解决方案
它内置了
在下列情形中,可以给任何元素和组件添加 entering/leaving 过渡:
- 条件渲染 (使用 v-if)
- 条件展示 (使用 v-show)
- 动态组件
-
例子
let vm = new Vue({el: '#app',template: '<div id="demo">' +'<button v-on:click="show = !show">' +'Toggle' +'</button>' +'<transition :appear="true" name="fade">' +'<p v-if="show">hello</p>' +'</transition>' +'</div>',data() {return {show: true}}})
.fade-enter-active, .fade-leave-active {transition: opacity .5s;}.fade-enter, .fade-leave-to {opacity: 0;}
内置组件
组件和 组件一样,都是 Vue 的内置组件 的定义在 src/platforms/web/runtime/component/transtion.js 中
之所以在这里定义,是因为组件是 web 平台独有的,先来看一下它的实现 export default {name: 'transition',props: transitionProps,abstract: true, // 抽象组件// render函数 - 渲染生成vnoderender (h: Function) {// 1.处理children// 默认插槽let children: any = this.$slots.default // 从默认插槽中获取<transition>包裹的子节点if (!children) {return}// filter out text nodes (possible whitespaces)children = children.filter(isNotTextNode)/* istanbul ignore if */// 长度为0直接返回if (!children.length) {return}// warn multiple elements// 长度大于1在开发环境报警告 - <transition>组件是只能包裹一个子节点的if (process.env.NODE_ENV !== 'production' && children.length > 1) {warn('<transition> can only be used on a single element. Use ' +'<transition-group> for lists.',this.$parent)}// 2.处理modelconst mode: string = this.mode// warn invalid mode// 过渡组件的对mode的支持只有2种 in-out或者是 out-inif (process.env.NODE_ENV !== 'production' &&mode && mode !== 'in-out' && mode !== 'out-in') {warn('invalid <transition> mode: ' + mode,this.$parent)}// 3.获取rawChild和childconst rawChild: VNode = children[0] // 第一个子节点vnode// if this is a component root node and the component's// parent container node also has transition, skip.// 判断当前<transition>如果是组件根节点并且外面包裹该组件的容器也是<transition>时要跳过if (hasParentTransition(this.$vnode)) {return rawChild}// apply transition data to child// use getRealChild() to ignore abstract components e.g. keep-alive// getRealChild的目的是获取组件的非抽象子节点 - <transition>很可能会包裹一个keep-aliveconst child: ?VNode = getRealChild(rawChild)/* istanbul ignore if */if (!child) {return rawChild}if (this._leaving) {return placeholder(h, rawChild)}// 4.处理id和data// 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.// 根据key等一系列条件获取idconst id: string = `__transition-${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// 通过extractTransitionData组件实例上提取出过渡所需要的数据const data: Object = (child.data || (child.data = {})).transition = extractTransitionData(this)const oldRawChild: VNode = this._vnodeconst oldChild: VNode = getRealChild(oldRawChild)// mark v-show// so that the transition module can hand over the control to the directive// child如果使用了v-show指令,也会把child.data.show设置为trueif (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!const oldData: Object = oldChild.data.transition = extend({}, data)// handle transition modeif (mode === 'out-in') {// return placeholder node and queue update when leave finishesthis._leaving = truemergeVNodeHook(oldData, 'afterLeave', () => {this._leaving = falsethis.$forceUpdate()})return placeholder(h, rawChild)} else if (mode === 'in-out') {if (isAsyncPlaceholder(child)) {return oldRawChild}let delayedLeaveconst performLeave = () => { delayedLeave() }mergeVNodeHook(data, 'afterEnter', performLeave)mergeVNodeHook(data, 'enterCancelled', performLeave)mergeVNodeHook(oldData, 'delayLeave', leave => { delayedLeave = leave })}}return rawChild}}
组件非常灵活,支持的 props 非常多 export const transitionProps = {name: String,appear: Boolean,css: Boolean,mode: String,type: String,enterClass: String,leaveClass: String,enterToClass: String,leaveToClass: String,enterActiveClass: String,leaveActiveClass: String,appearClass: String,appearActiveClass: String,appearToClass: String,duration: [Number, String, Object]}
hasParentTransition
function hasParentTransition (vnode: VNode): ?boolean {while ((vnode = vnode.parent)) {if (vnode.data.transition) {return true}}}
因为传入的是 this.$vnode,也就是
组件的 占位 vnode,只有当它同时作为根 vnode,也就是 vm._vnode 的时候,它的 parent 才不会为空,并且判断 parent 也是 组件,才返回 true
getRealChild// in case the child is also an abstract component, e.g. <keep-alive>// we want to recursively retrieve the real component to be renderedfunction getRealChild (vnode: ?VNode): ?VNode {const compOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions// 递归找到第一个非抽象组件的vnode并返回if (compOptions && compOptions.Ctor.options.abstract) {return getRealChild(getFirstComponentChild(compOptions.children))} else {return vnode}}
extractTransitionData
export function extractTransitionData (comp: Component): Object {const data = {}const options: ComponentOptions = comp.$options// props 遍历props赋值到data中for (const key in options.propsData) {data[key] = comp[key]}// events.// extract listeners and pass them directly to the transition methodsconst listeners: ?Object = options._parentListeners// 遍历所有父组件的事件把事件回调赋值到data中for (const key in listeners) {data[camelize(key)] = listeners[key]}return data}
这样 child.data.transition 中就包含了过渡所需的一些数据
在例子中得到的child.data如下{transition: {appear: true,name: 'fade'}}
transition module
组件的实现,它的 render 阶段只获取了一些数据,并且返回了渲染的 vnode,并没有任何和动画相关,而动画相关的逻辑全部在 src/platforms/web/modules/transition.js 中 ```javascript function enter (: any, vnode: VNodeWithData) { if (vnode.data.show !== true) { enter(vnode) } }
export default inBrowser ? { create: _enter, activate: _enter, remove (vnode: VNode, rm: Function) { / istanbul ignore else / if (vnode.data.show !== true) { leave(vnode, rm) } else { rm() } } } : {}
对于过渡的实现,它只接收了 create 和 activate 2 个钩子函数,知道 create 钩子函数只有当节点的创建过程才会执行,而 remove 会在节点销毁的时候执行,这也就印证了 <transition> 必须要满足 v-if 、动态组件、组件根节点条件之一了,对于 v-show 在它的指令的钩子函数中也会执行相关逻辑<br />**过渡动画提供了 2 个时机,一个是 create 和 activate 的时候提供了 entering 进入动画,一个是 remove 的时候提供了 leaving 离开动画**<a name="Rd57n"></a>## entering整个 entering 过程的实现是 enter 函数```javascriptexport function enter (vnode: VNodeWithData, toggleDisplay: ?() => void) {const el: any = vnode.elm// call leave callback nowif (isDef(el._leaveCb)) {el._leaveCb.cancelled = trueel._leaveCb()}// 1.解析过渡数据const data = resolveTransition(vnode.data.transition) // 解析出过渡相关的一些数据if (isUndef(data)) {return}/* istanbul ignore if */if (isDef(el._enterCb) || el.nodeType !== 1) {return}const {css,type,enterClass,enterToClass,enterActiveClass,appearClass,appearToClass,appearActiveClass,beforeEnter,enter,afterEnter,enterCancelled,beforeAppear,appear,afterAppear,appearCancelled,duration} = data// 2.处理边界情况// 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.// 当<transition>作为子组件的根节点时检查它的父组件作为appear的检查// isAppear表示当前上下文实例还没有mounted,第一次出现的时候let context = activeInstancelet transitionNode = activeInstance.$vnodewhile (transitionNode && transitionNode.parent) {context = transitionNode.contexttransitionNode = transitionNode.parent}const isAppear = !context._isMounted || !vnode.isRootInsert// 如果是第一次且<transition>组件没有配置appear的话直接返回if (isAppear && !appear && appear !== '') {return}// 3.定义过渡类名、钩子函数和其它配置// startClass定义进入过渡的开始状态,在元素被插入时生效,在下一个帧移除const startClass = isAppear && appearClass? appearClass: enterClass// activeClass定义过渡的状态,在元素整个过渡过程中作用,在元素被插入时生效,在transtion/animation完成之后移除const activeClass = isAppear && appearActiveClass? appearActiveClass: enterActiveClass// toClass定义进入过渡的结束状态,在元素被插入一帧后生效(与此同时startClass被删除),在transtion/animation完成之后移除const toClass = isAppear && appearToClass? appearToClass: enterToClass// beforeEnterHook是过渡开始前执行的钩子函数const beforeEnterHook = isAppear? (beforeAppear || beforeEnter): beforeEnter// enterHook是在元素插入后或者是v-show显示切换后执行的钩子函数const enterHook = isAppear? (typeof appear === 'function' ? appear : enter): enter// afterEnterHook是在过渡动画执行完后的钩子函数const afterEnterHook = isAppear? (afterAppear || afterEnter): afterEnterconst enterCancelledHook = isAppear? (appearCancelled || enterCancelled): enterCancelled// explicitEnterDuration表示enter动画执行的时间const explicitEnterDuration: any = toNumber(isObject(duration)? duration.enter: duration)if (process.env.NODE_ENV !== 'production' && explicitEnterDuration != null) {checkDuration(explicitEnterDuration, 'enter', vnode)}// expectsCSS表示过渡动画是受CSS的影响const expectsCSS = css !== false && !isIE9const userWantsControl = getHookArgumentsLength(enterHook)// cb定义的是过渡完成执行的回调函数const cb = el._enterCb = once(() => {if (expectsCSS) {// 把toClass和activeClass移除removeTransitionClass(el, toClass)removeTransitionClass(el, activeClass)}if (cb.cancelled) {// 取消则移除startClass并执行enterCancelledHookif (expectsCSS) {removeTransitionClass(el, startClass)}enterCancelledHook && enterCancelledHook(el)} else {// 没有取消afterEnterHook && afterEnterHook(el)}el._enterCb = null})// 4.合并insert钩子函数if (!vnode.data.show) {// remove pending leave element on enter by injecting an insert hookmergeVNodeHook(vnode, 'insert', () => {const parent = el.parentNodeconst pendingNode = parent && parent._pending && parent._pending[vnode.key]if (pendingNode &&pendingNode.tag === vnode.tag &&pendingNode.elm._leaveCb) {pendingNode.elm._leaveCb()}enterHook && enterHook(el, cb)})}// 5.开始执行过渡动画// start enter transitionbeforeEnterHook && beforeEnterHook(el)if (expectsCSS) { // 为true表面希望用CSS来控制动画// 添加了startClass和activeClassaddTransitionClass(el, startClass)addTransitionClass(el, activeClass)// nextFrame是requestAnimationFrame的实现// 下一帧nextFrame(() => {// 把 startClass 移除removeTransitionClass(el, startClass) // 移除fade-enter样式// 过渡没有被取消if (!cb.cancelled) {addTransitionClass(el, toClass) // 添加toClass 例子中添加fade-enter-to// 用户不通过enterHook钩子函数控制动画if (!userWantsControl) {if (isValidDuration(explicitEnterDuration)) {setTimeout(cb, explicitEnterDuration) // 用户指定了explicitEnterDuration则延时这个时间执行cb} else {whenTransitionEnds(el, type, cb) // 决定执行cb的时机}}}})}if (vnode.data.show) {toggleDisplay && toggleDisplay()enterHook && enterHook(el, cb)}if (!expectsCSS && !userWantsControl) {cb()}}
resolveTransition定义在 src/platforms/web/transition-util.js 中
export function resolveTransition (def?: string | Object): ?Object {if (!def) {return}/* istanbul ignore else */if (typeof def === 'object') {const res = {}// 通过autoCssTransition处理name属性生成一个用来描述各个阶段的Class名称的对象,扩展到def中并返回给data,这样就可以从data中获取到过渡相关的所有数据if (def.css !== false) {extend(res, autoCssTransition(def.name || 'v'))}extend(res, def)return res} else if (typeof def === 'string') {return autoCssTransition(def)}}const autoCssTransition: (name: string) => Object = cached(name => {return {enterClass: `${name}-enter`,enterToClass: `${name}-enter-to`,enterActiveClass: `${name}-enter-active`,leaveClass: `${name}-leave`,leaveToClass: `${name}-leave-to`,leaveActiveClass: `${name}-leave-active`}})
mergeVNodeHook定义在 src/core/vdom/helpers/merge-hook.js 中
export function mergeVNodeHook (def: Object, hookKey: string, hook: Function) {// 把 hook 函数合并到 def.data.hook[hookey] 中,生成新的 invokerif (def instanceof VNode) {def = def.data.hook || (def.data.hook = {})}let invokerconst oldHook = def[hookKey]function wrappedHook () {hook.apply(this, arguments)// important: remove merged hook to ensure it's called only once// and prevent memory leakremove(invoker.fns, wrappedHook)}if (isUndef(oldHook)) {// no existing hookinvoker = createFnInvoker([wrappedHook])} else {/* istanbul ignore if */if (isDef(oldHook.fns) && isTrue(oldHook.merged)) {// already a merged invokerinvoker = oldHookinvoker.fns.push(wrappedHook)} else {// existing plain hookinvoker = createFnInvoker([oldHook, wrappedHook])}}invoker.merged = truedef[hookKey] = invoker}
vnode 原本定义了 init、prepatch、insert、destroy 四个钩子函数,而 mergeVNodeHook 函数就是把一些新的钩子函数合并进来,例如在
addTransitionClass定义在 src/platforms/web/runtime/transition-util.js 中
export function addTransitionClass (el: any, cls: string) {// 给当前 DOM 元素 el 添加样式 clsconst transitionClasses = el._transitionClasses || (el._transitionClasses = [])if (transitionClasses.indexOf(cls) < 0) {transitionClasses.push(cls)addClass(el, cls)}}
nextFrame
// binding to window is necessary to make hot reload work in IE in strict mode// 简单的requestAnimationFrame的实现,它的参数fn会在下一帧执行const raf = inBrowser? window.requestAnimationFrame? window.requestAnimationFrame.bind(window): setTimeout: /* istanbul ignore next */ fn => fn()export function nextFrame (fn: Function) {raf(() => {raf(fn)})}
removeTransitionClass
export function removeTransitionClass (el: any, cls: string) {if (el._transitionClasses) {remove(el._transitionClasses, cls)}removeClass(el, cls)}
whenTransitionEnds
本质上就利用了过渡动画的结束事件来决定 cb 函数的执行
export function whenTransitionEnds (el: Element,expectedType: ?string,cb: Function) {const { type, timeout, propCount } = getTransitionInfo(el, expectedType)if (!type) return cb()const event: string = type === TRANSITION ? transitionEndEvent : animationEndEventlet ended = 0const end = () => {el.removeEventListener(event, onEnd)cb()}const onEnd = e => {if (e.target === el) {if (++ended >= propCount) {end()}}}setTimeout(() => {if (ended < propCount) {end()}}, timeout + 1)el.addEventListener(event, onEnd)}
leaving
与 entering 相对的就是 leaving 阶段了,entering 主要发生在组件插入后,而 leaving 主要发生在组件销毁前
export function leave (vnode: VNodeWithData, rm: Function) {const el: any = vnode.elm// call enter callback nowif (isDef(el._enterCb)) {el._enterCb.cancelled = trueel._enterCb()}const data = resolveTransition(vnode.data.transition)if (isUndef(data) || el.nodeType !== 1) {return rm()}/* istanbul ignore if */if (isDef(el._leaveCb)) {return}const {css,type,leaveClass,leaveToClass,leaveActiveClass,beforeLeave,leave,afterLeave,leaveCancelled,delayLeave,duration} = dataconst expectsCSS = css !== false && !isIE9const userWantsControl = getHookArgumentsLength(leave)const explicitLeaveDuration: any = toNumber(isObject(duration)? duration.leave: duration)if (process.env.NODE_ENV !== 'production' && isDef(explicitLeaveDuration)) {checkDuration(explicitLeaveDuration, 'leave', vnode)}const cb = el._leaveCb = once(() => {if (el.parentNode && el.parentNode._pending) {el.parentNode._pending[vnode.key] = null}if (expectsCSS) {removeTransitionClass(el, leaveToClass)removeTransitionClass(el, leaveActiveClass)}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 cancelledif (cb.cancelled) {return}// record leaving elementif (!vnode.data.show && el.parentNode) {(el.parentNode._pending || (el.parentNode._pending = {}))[(vnode.key: any)] = vnode}beforeLeave && beforeLeave(el)if (expectsCSS) {addTransitionClass(el, leaveClass)addTransitionClass(el, leaveActiveClass)nextFrame(() => {removeTransitionClass(el, leaveClass)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()}}}
纵观 leave 的实现,和 enter 的实现几乎是一个镜像过程,不同的是从 data 中解析出来的是 leave 相关的样式类名和钩子函数
还有一点不同是可以配置 delayLeave,它是一个函数,可以延时执行 leave 的相关过渡动画,在 leave 动画执行完后,它会执行 rm 函数把节点从 DOM 中真正做移除
Vue 的过渡实现分为以下几个步骤:
- 自动嗅探目标元素是否应用了 CSS 过渡或动画,如果是,在恰当的时机添加/删除 CSS 类名
- 如果过渡组件提供了 JavaScript 钩子函数,这些钩子函数将在恰当的时机被调用
- 如果没有找到 JavaScript 钩子并且也没有检测到 CSS 过渡/动画,DOM 操作 (插入/删除) 在下一帧中立即执行
所以真正执行动画的是我们写的 CSS 或者是 JavaScript 钩子函数,而 Vue 的
