# transition
在我们平时的前端项目开发中,经常会遇到如下需求,一个 DOM 节点的插入和删除或者是显示和隐藏,我们不想让它特别生硬,通常会考虑加一些过渡效果。
Vue.js 除了实现了强大的数据驱动,组件化的能力,也给我们提供了一整套过渡的解决方案。它内置了 <transition>
组件,我们可以利用它配合一些 CSS3 样式很方便地实现过渡动画,也可以利用它配合 JavaScript 的钩子函数实现过渡动画,在下列情形中,可以给任何元素和组件添加 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
}
}
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
.fade-enter-active, .fade-leave-active {
transition: opacity .5s;
}
.fade-enter, .fade-leave-to {
opacity: 0;
}
2
3
4
5
6
当我们点击按钮切换显示状态的时候,被 <transition>
包裹的内容会有过渡动画。那么接下来我们从源码的角度来分析它的实现原理。
# 内置组件
<transition>
组件和 <keep-alive>
组件一样,都是 Vue 的内置组件,而 <transition>
的定义在 src/platforms/web/runtime/component/transtion.js
中,之所以在这里定义,是因为 <transition>
组件是 web 平台独有的,先来看一下它的实现:
export default {
name: 'transition',
props: transitionProps,
abstract: true,
render (h: Function) {
let children: any = this.$slots.default
if (!children) {
return
}
// filter out text nodes (possible whitespaces)
children = children.filter((c: VNode) => c.tag || isAsyncPlaceholder(c))
/* istanbul ignore if */
if (!children.length) {
return
}
// warn multiple elements
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
)
}
const mode: string = this.mode
// warn invalid mode
if (process.env.NODE_ENV !== 'production' &&
mode && mode !== 'in-out' && mode !== 'out-in'
) {
warn(
'invalid <transition> mode: ' + mode,
this.$parent
)
}
const rawChild: VNode = 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
const child: ?VNode = 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.
const 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
const data: Object = (child.data || (child.data = {})).transition = extractTransitionData(this)
const oldRawChild: VNode = this._vnode
const oldChild: VNode = 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(d => d.name === 'show')) {
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 mode
if (mode === 'out-in') {
// return placeholder node and queue update when leave finishes
this._leaving = true
mergeVNodeHook(oldData, 'afterLeave', () => {
this._leaving = false
this.$forceUpdate()
})
return placeholder(h, rawChild)
} else if (mode === 'in-out') {
if (isAsyncPlaceholder(child)) {
return oldRawChild
}
let delayedLeave
const performLeave = () => { delayedLeave() }
mergeVNodeHook(data, 'afterEnter', performLeave)
mergeVNodeHook(data, 'enterCancelled', performLeave)
mergeVNodeHook(oldData, 'delayLeave', leave => { delayedLeave = leave })
}
}
return rawChild
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
<transition>
组件和 <keep-alive>
组件有几点实现类似,同样是抽象组件,同样直接实现 render
函数,同样利用了默认插槽。<transition>
组件非常灵活,支持的 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]
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
这些配置我们稍后会分析它们的作用,<transition>
组件另一个重要的就是 render
函数的实现,render
函数主要作用就是渲染生成 vnode
,下面来看一下这部分的逻辑。
- 处理
children
let children: any = this.$slots.default
if (!children) {
return
}
// filter out text nodes (possible whitespaces)
children = children.filter((c: VNode) => c.tag || isAsyncPlaceholder(c))
/* istanbul ignore if */
if (!children.length) {
return
}
// warn multiple elements
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
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
先从默认插槽中获取 <transition>
包裹的子节点,并且判断了子节点的长度,如果长度为 0,则直接返回,否则判断长度如果大于 1,也会在开发环境报警告,因为 <transition>
组件是只能包裹一个子节点的。
- 处理
model
const mode: string = this.mode
// warn invalid mode
if (process.env.NODE_ENV !== 'production' &&
mode && mode !== 'in-out' && mode !== 'out-in'
) {
warn(
'invalid <transition> mode: ' + mode,
this.$parent
)
}
2
3
4
5
6
7
8
9
10
11
过渡组件的对 mode
的支持只有 2 种,in-out
或者是 out-in
。
- 获取
rawChild
&child
const rawChild: VNode = 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
const child: ?VNode = getRealChild(rawChild)
/* istanbul ignore if */
if (!child) {
return rawChild
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
rawChild
就是第一个子节点 vnode
,接着判断当前 <transition>
如果是组件根节点并且外面包裹该组件的容器也是 <transition>
的时候要跳过。来看一下 hasParentTransition
的实现:
function hasParentTransition (vnode: VNode): ?boolean {
while ((vnode = vnode.parent)) {
if (vnode.data.transition) {
return true
}
}
}
2
3
4
5
6
7
因为传入的是 this.$vnode
,也就是 <transition>
组件的 占位 vnode
,只有当它同时作为根 vnode
,也就是 vm._vnode
的时候,它的 parent
才不会为空,并且判断 parent
也是 <transition>
组件,才返回 true,vnode.data.transition
我们稍后会介绍。
getRealChild
的目的是获取组件的非抽象子节点,因为 <transition>
很可能会包裹一个 keep-alive
,它的实现如下:
// in case the child is also an abstract component, e.g. <keep-alive>
// we want to recursively retrieve the real component to be rendered
function getRealChild (vnode: ?VNode): ?VNode {
const compOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
if (compOptions && compOptions.Ctor.options.abstract) {
return getRealChild(getFirstComponentChild(compOptions.children))
} else {
return vnode
}
}
2
3
4
5
6
7
8
9
10
会递归找到第一个非抽象组件的 vnode
并返回,在我们这个 case 下,rawChild === child
。
- 处理
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.
const 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
const data: Object = (child.data || (child.data = {})).transition = extractTransitionData(this)
const oldRawChild: VNode = this._vnode
const oldChild: VNode = 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(d => d.name === 'show')) {
child.data.show = true
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
先根据 key
等一系列条件获取 id
,接着从当前通过 extractTransitionData
组件实例上提取出过渡所需要的数据:
export function extractTransitionData (comp: Component): Object {
const data = {}
const options: ComponentOptions = comp.$options
// props
for (const key in options.propsData) {
data[key] = comp[key]
}
// events.
// extract listeners and pass them directly to the transition methods
const listeners: ?Object = options._parentListeners
for (const key in listeners) {
data[camelize(key)] = listeners[key]
}
return data
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
首先是遍历 props
赋值到 data
中,接着是遍历所有父组件的事件也把事件回调赋值到 data
中。
这样 child.data.transition
中就包含了过渡所需的一些数据,这些稍后都会用到,对于 child
如果使用了 v-show
指令,也会把 child.data.show
设置为 true,在我们的例子中,得到的 child.data
如下:
{
transition: {
appear: true,
name: 'fade'
}
}
2
3
4
5
6
至于 oldRawChild
和 oldChild
是与后面的判断逻辑相关,这些我们这里先不介绍。
# transition module
刚刚我们介绍完 <transition>
组件的实现,它的 render
阶段只获取了一些数据,并且返回了渲染的 vnode
,并没有任何和动画相关,而动画相关的逻辑全部在 src/platforms/web/modules/transition.js
中:
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()
}
}
} : {}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
在之前介绍事件实现的章节中我们提到过在 vnode patch
的过程中,会执行很多钩子函数,那么对于过渡的实现,它只接收了 create
和 activate
2 个钩子函数,我们知道 create
钩子函数只有当节点的创建过程才会执行,而 remove
会在节点销毁的时候执行,这也就印证了 <transition>
必须要满足 v-if
、动态组件、组件根节点条件之一了,对于 v-show
在它的指令的钩子函数中也会执行相关逻辑,这块儿先不介绍。
过渡动画提供了 2 个时机,一个是 create
和 activate
的时候提供了 entering 进入动画,一个是 remove
的时候提供了 leaving 离开动画,那么接下来我们就来分别去分析这两个过程。
# entering
整个 entering 过程的实现是 enter
函数:
export function enter (vnode: VNodeWithData, toggleDisplay: ?() => void) {
const el: any = vnode.elm
// call leave callback now
if (isDef(el._leaveCb)) {
el._leaveCb.cancelled = true
el._leaveCb()
}
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
// 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.
let context = activeInstance
let transitionNode = activeInstance.$vnode
while (transitionNode && transitionNode.parent) {
transitionNode = transitionNode.parent
context = transitionNode.context
}
const isAppear = !context._isMounted || !vnode.isRootInsert
if (isAppear && !appear && appear !== '') {
return
}
const startClass = isAppear && appearClass
? appearClass
: enterClass
const activeClass = isAppear && appearActiveClass
? appearActiveClass
: enterActiveClass
const toClass = isAppear && appearToClass
? appearToClass
: enterToClass
const beforeEnterHook = isAppear
? (beforeAppear || beforeEnter)
: beforeEnter
const enterHook = isAppear
? (typeof appear === 'function' ? appear : enter)
: enter
const afterEnterHook = isAppear
? (afterAppear || afterEnter)
: afterEnter
const enterCancelledHook = isAppear
? (appearCancelled || enterCancelled)
: enterCancelled
const explicitEnterDuration: any = toNumber(
isObject(duration)
? duration.enter
: duration
)
if (process.env.NODE_ENV !== 'production' && explicitEnterDuration != null) {
checkDuration(explicitEnterDuration, 'enter', vnode)
}
const expectsCSS = css !== false && !isIE9
const userWantsControl = getHookArgumentsLength(enterHook)
const cb = el._enterCb = once(() => {
if (expectsCSS) {
removeTransitionClass(el, toClass)
removeTransitionClass(el, activeClass)
}
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', () => {
const parent = el.parentNode
const 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(() => {
removeTransitionClass(el, startClass)
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()
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
enter
的代码很长,我们先分析其中的核心逻辑。
- 解析过渡数据
const data = resolveTransition(vnode.data.transition)
if (isUndef(data)) {
return
}
const {
css,
type,
enterClass,
enterToClass,
enterActiveClass,
appearClass,
appearToClass,
appearActiveClass,
beforeEnter,
enter,
afterEnter,
enterCancelled,
beforeAppear,
appear,
afterAppear,
appearCancelled,
duration
} = data
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
从 vnode.data.transition
中解析出过渡相关的一些数据,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 = {}
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`
}
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
resolveTransition
会通过 autoCssTransition
处理 name
属性,生成一个用来描述各个阶段的 Class
名称的对象,扩展到 def
中并返回给 data
,这样我们就可以从 data
中获取到过渡相关的所有数据。
- 处理边界情况
// 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.
let context = activeInstance
let transitionNode = activeInstance.$vnode
while (transitionNode && transitionNode.parent) {
transitionNode = transitionNode.parent
context = transitionNode.context
}
const isAppear = !context._isMounted || !vnode.isRootInsert
if (isAppear && !appear && appear !== '') {
return
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
这是为了处理当 <transition>
作为子组件的根节点,那么我们需要检查它的父组件作为 appear
的检查。isAppear
表示当前上下文实例还没有 mounted
,第一次出现的时机。如果是第一次并且 <transition>
组件没有配置 appear
的话,直接返回。
- 定义过渡类名、钩子函数和其它配置
const startClass = isAppear && appearClass
? appearClass
: enterClass
const activeClass = isAppear && appearActiveClass
? appearActiveClass
: enterActiveClass
const toClass = isAppear && appearToClass
? appearToClass
: enterToClass
const beforeEnterHook = isAppear
? (beforeAppear || beforeEnter)
: beforeEnter
const enterHook = isAppear
? (typeof appear === 'function' ? appear : enter)
: enter
const afterEnterHook = isAppear
? (afterAppear || afterEnter)
: afterEnter
const enterCancelledHook = isAppear
? (appearCancelled || enterCancelled)
: enterCancelled
const explicitEnterDuration: any = toNumber(
isObject(duration)
? duration.enter
: duration
)
if (process.env.NODE_ENV !== 'production' && explicitEnterDuration != null) {
checkDuration(explicitEnterDuration, 'enter', vnode)
}
const expectsCSS = css !== false && !isIE9
const userWantsControl = getHookArgumentsLength(enterHook)
const cb = el._enterCb = once(() => {
if (expectsCSS) {
removeTransitionClass(el, toClass)
removeTransitionClass(el, activeClass)
}
if (cb.cancelled) {
if (expectsCSS) {
removeTransitionClass(el, startClass)
}
enterCancelledHook && enterCancelledHook(el)
} else {
afterEnterHook && afterEnterHook(el)
}
el._enterCb = null
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
对于过渡类名方面,startClass
定义进入过渡的开始状态,在元素被插入时生效,在下一个帧移除;activeClass
定义过渡的状态,在元素整个过渡过程中作用,在元素被插入时生效,在 transition/animation
完成之后移除;toClass
定义进入过渡的结束状态,在元素被插入一帧后生效 (与此同时 startClass
被删除),在 <transition>/animation
完成之后移除。
对于过渡钩子函数方面,beforeEnterHook
是过渡开始前执行的钩子函数,enterHook
是在元素插入后或者是 v-show
显示切换后执行的钩子函数。afterEnterHook
是在过渡动画执行完后的钩子函数。
explicitEnterDuration
表示 enter 动画执行的时间。
expectsCSS
表示过渡动画是受 CSS 的影响。
cb
定义的是过渡完成执行的回调函数。
- 合并
insert
钩子函数
if (!vnode.data.show) {
// remove pending leave element on enter by injecting an insert hook
mergeVNodeHook(vnode, 'insert', () => {
const parent = el.parentNode
const pendingNode = parent && parent._pending && parent._pending[vnode.key]
if (pendingNode &&
pendingNode.tag === vnode.tag &&
pendingNode.elm._leaveCb
) {
pendingNode.elm._leaveCb()
}
enterHook && enterHook(el, cb)
})
}
2
3
4
5
6
7
8
9
10
11
12
13
14
mergeVNodeHook
的定义在 src/core/vdom/helpers/merge-hook.js
中:
export function mergeVNodeHook (def: Object, hookKey: string, hook: Function) {
if (def instanceof VNode) {
def = def.data.hook || (def.data.hook = {})
}
let invoker
const oldHook = def[hookKey]
function wrappedHook () {
hook.apply(this, arguments)
// important: remove merged hook to ensure it's called only once
// and prevent memory leak
remove(invoker.fns, wrappedHook)
}
if (isUndef(oldHook)) {
// no existing hook
invoker = createFnInvoker([wrappedHook])
} else {
/* istanbul ignore if */
if (isDef(oldHook.fns) && isTrue(oldHook.merged)) {
// already a merged invoker
invoker = oldHook
invoker.fns.push(wrappedHook)
} else {
// existing plain hook
invoker = createFnInvoker([oldHook, wrappedHook])
}
}
invoker.merged = true
def[hookKey] = invoker
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
mergeVNodeHook
的逻辑很简单,就是把 hook
函数合并到 def.data.hook[hookey]
中,生成新的 invoker
,createFnInvoker
方法我们在分析事件章节的时候已经介绍过了。
我们之前知道组件的 vnode
原本定义了 init
、prepatch
、insert
、destroy
四个钩子函数,而 mergeVNodeHook
函数就是把一些新的钩子函数合并进来,例如在 <transition>
过程中合并的 insert
钩子函数,就会合并到组件 vnode
的 insert
钩子函数中,这样当组件插入后,就会执行我们定义的 enterHook
了。
- 开始执行过渡动画
// start enter transition
beforeEnterHook && beforeEnterHook(el)
if (expectsCSS) {
addTransitionClass(el, startClass)
addTransitionClass(el, activeClass)
nextFrame(() => {
removeTransitionClass(el, startClass)
if (!cb.cancelled) {
addTransitionClass(el, toClass)
if (!userWantsControl) {
if (isValidDuration(explicitEnterDuration)) {
setTimeout(cb, explicitEnterDuration)
} else {
whenTransitionEnds(el, type, cb)
}
}
}
})
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
首先执行 beforeEnterHook
钩子函数,把当前元素的 DOM 节点 el
传入,然后判断 expectsCSS
,如果为 true 则表明希望用 CSS 来控制动画,那么会执行 addTransitionClass(el, startClass)
和 addTransitionClass(el, activeClass)
,它的定义在 src/platforms/runtime/transition-util.js
中:
export function addTransitionClass (el: any, cls: string) {
const transitionClasses = el._transitionClasses || (el._transitionClasses = [])
if (transitionClasses.indexOf(cls) < 0) {
transitionClasses.push(cls)
addClass(el, cls)
}
}
2
3
4
5
6
7
其实非常简单,就是给当前 DOM 元素 el
添加样式 cls
,所以这里添加了 startClass
和 activeClass
,在我们的例子中就是给 p
标签添加了 fade-enter
和 fade-enter-active
2 个样式。
接下来执行了 nextFrame
:
const raf = inBrowser
? window.requestAnimationFrame
? window.requestAnimationFrame.bind(window)
: setTimeout
: fn => fn()
export function nextFrame (fn: Function) {
raf(() => {
raf(fn)
})
}
2
3
4
5
6
7
8
9
10
11
它就是一个简单的 requestAnimationFrame
的实现,它的参数 fn 会在下一帧执行,因此下一帧执行了 removeTransitionClass(el, startClass)
:
export function removeTransitionClass (el: any, cls: string) {
if (el._transitionClasses) {
remove(el._transitionClasses, cls)
}
removeClass(el, cls)
}
2
3
4
5
6
把 startClass
移除,在我们的等例子中就是移除 fade-enter
样式。然后判断此时过渡没有被取消,则执行 addTransitionClass(el, toClass)
添加 toClass
,在我们的例子中就是添加了 fade-enter-to
。然后判断 !userWantsControl
,也就是用户不通过 enterHook
钩子函数控制动画,这时候如果用户指定了 explicitEnterDuration
,则延时这个时间执行 cb
,否则通过 whenTransitionEnds(el, type, cb)
决定执行 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 : animationEndEvent
let ended = 0
const 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)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
whenTransitionEnds
的逻辑具体不深讲了,本质上就利用了过渡动画的结束事件来决定 cb
函数的执行。
最后再回到 cb
函数:
const cb = el._enterCb = once(() => {
if (expectsCSS) {
removeTransitionClass(el, toClass)
removeTransitionClass(el, activeClass)
}
if (cb.cancelled) {
if (expectsCSS) {
removeTransitionClass(el, startClass)
}
enterCancelledHook && enterCancelledHook(el)
} else {
afterEnterHook && afterEnterHook(el)
}
el._enterCb = null
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
其实很简单,执行了 removeTransitionClass(el, toClass)
和 removeTransitionClass(el, activeClass)
把 toClass
和 activeClass
移除,然后判断如果有没有取消,如果取消则移除 startClass
并执行 enterCancelledHook
,否则执行 afterEnterHook(el)
。
那么到这里,entering
的过程就介绍完了。
# leaving
与 entering
相对的就是 leaving
阶段了,entering
主要发生在组件插入后,而 leaving
主要发生在组件销毁前。
export function leave (vnode: VNodeWithData, rm: Function) {
const el: any = vnode.elm
// call enter callback now
if (isDef(el._enterCb)) {
el._enterCb.cancelled = true
el._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
} = data
const expectsCSS = css !== false && !isIE9
const 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 cancelled
if (cb.cancelled) {
return
}
// record leaving element
if (!vnode.data.show) {
(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()
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
纵观 leave
的实现,和 enter
的实现几乎是一个镜像过程,不同的是从 data
中解析出来的是 leave
相关的样式类名和钩子函数。还有一点不同是可以配置 delayLeave
,它是一个函数,可以延时执行 leave
的相关过渡动画,在 leave
动画执行完后,它会执行 rm
函数把节点从 DOM 中真正做移除。
# 总结
那么到此为止基本的 <transition>
过渡的实现分析完毕了,总结起来,Vue 的过渡实现分为以下几个步骤:
自动嗅探目标元素是否应用了 CSS 过渡或动画,如果是,在恰当的时机添加/删除 CSS 类名。
如果过渡组件提供了 JavaScript 钩子函数,这些钩子函数将在恰当的时机被调用。
如果没有找到 JavaScript 钩子并且也没有检测到 CSS 过渡/动画,DOM 操作 (插入/删除) 在下一帧中立即执行。
所以真正执行动画的是我们写的 CSS 或者是 JavaScript 钩子函数,而 Vue 的 <transition>
只是帮我们很好地管理了这些 CSS 的添加/删除,以及钩子函数的执行时机。