Skip to content

Commit d4578da

Browse files
committed
refactor CSS transition handling
1 parent 37077ee commit d4578da

File tree

2 files changed

+182
-120
lines changed

2 files changed

+182
-120
lines changed

src/transition/css.js

Lines changed: 110 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,122 @@ var _ = require('../util')
22
var transDurationProp = _.transitionProp + 'Duration'
33
var animDurationProp = _.animationProp + 'Duration'
44

5+
var queue = []
6+
var queued = false
7+
58
/**
6-
* Force layout before triggering transitions/animations
9+
* Push a job into the transition queue, which is to be
10+
* executed on next frame.
11+
*
12+
* @param {Element} el - target element
13+
* @param {Number} dir - 1: enter, -1: leave
14+
* @param {Function} op - the actual dom operation
15+
* @param {String} cls - the className to remove when the
16+
* transition is done.
17+
* @param {Function} [cb] - user supplied callback.
718
*/
819

9-
var justReflowed = false
20+
function push (el, dir, op, cls, cb) {
21+
queue.push({
22+
el : el,
23+
dir : dir,
24+
cb : cb,
25+
cls : cls,
26+
op : op
27+
})
28+
if (!queued) {
29+
queued = true
30+
_.nextTick(flush)
31+
}
32+
}
33+
34+
/**
35+
* Flush the queue, and do one forced reflow before
36+
* triggering transitions.
37+
*/
1038

11-
function reflow () {
12-
if (justReflowed) return
13-
justReflowed = true
39+
function flush () {
1440
/* jshint unused: false */
1541
var f = document.documentElement.offsetHeight
16-
_.nextTick(unlock)
42+
queue.forEach(run)
43+
queue = []
44+
queued = false
1745
}
1846

19-
function unlock () {
20-
justReflowed = false
47+
/**
48+
* Run a transition job.
49+
*
50+
* @param {Object} job
51+
*/
52+
53+
function run (job) {
54+
55+
var el = job.el
56+
var classList = el.classList
57+
var data = el.__v_trans
58+
var cls = job.cls
59+
var cb = job.cb
60+
var op = job.op
61+
var transitionType = getTransitionType(el, data, cls)
62+
63+
if (job.dir > 0) { // ENTER
64+
if (transitionType === 1) {
65+
// trigger transition by removing enter class
66+
classList.remove(cls)
67+
// only need to listen for transitionend if there's
68+
// a user callback
69+
if (cb) setupTransitionCb(_.transitionEndEvent)
70+
} else if (transitionType === 2) {
71+
// animations are triggered when class is added
72+
// so we just listen for animationend to remove it.
73+
setupTransitionCb(_.animationEndEvent, function () {
74+
classList.remove(cls)
75+
})
76+
} else {
77+
// no transition applicable
78+
classList.remove(cls)
79+
if (cb) cb()
80+
}
81+
} else { // LEAVE
82+
if (transitionType) {
83+
// leave transitions/animations are both triggered
84+
// by adding the class, just remove it on end event.
85+
var event = transitionType === 1
86+
? _.transitionEndEvent
87+
: _.animationEndEvent
88+
setupTransitionCb(event, function () {
89+
op()
90+
classList.remove(cls)
91+
})
92+
} else {
93+
op()
94+
classList.remove(cls)
95+
if (cb) cb()
96+
}
97+
}
98+
99+
/**
100+
* Set up a transition end callback, store the callback
101+
* on the element's __v_trans data object, so we can
102+
* clean it up if another transition is triggered before
103+
* the callback is fired.
104+
*
105+
* @param {String} event
106+
* @param {Function} [cleanupFn]
107+
*/
108+
109+
function setupTransitionCb (event, cleanupFn) {
110+
data.event = event
111+
var onEnd = data.callback = function transitionCb (e) {
112+
if (e.target === el) {
113+
_.off(el, event, onEnd)
114+
data.event = data.callback = null
115+
if (cleanupFn) cleanupFn()
116+
if (cb) cb()
117+
}
118+
}
119+
_.on(el, event, onEnd)
120+
}
21121
}
22122

23123
/**
@@ -78,74 +178,12 @@ module.exports = function (el, direction, op, data, cb) {
78178
classList.remove(leaveClass)
79179
data.event = data.callback = null
80180
}
81-
var transitionType, onEnd, endEvent
82181
if (direction > 0) { // enter
83-
// Enter Transition
84182
classList.add(enterClass)
85183
op()
86-
transitionType = getTransitionType(el, data, enterClass)
87-
if (transitionType === 1) {
88-
reflow()
89-
classList.remove(enterClass)
90-
// only listen for transition end if user has sent
91-
// in a callback
92-
if (cb) {
93-
endEvent = data.event = _.transitionEndEvent
94-
onEnd = data.callback = function transitionCb (e) {
95-
if (e.target === el) {
96-
_.off(el, endEvent, onEnd)
97-
data.event = data.callback = null
98-
cb()
99-
}
100-
}
101-
_.on(el, endEvent, onEnd)
102-
}
103-
} else if (transitionType === 2) {
104-
// Enter Animation
105-
//
106-
// Animations are triggered automatically as the
107-
// element is inserted into the DOM, so we just
108-
// listen for the animationend event.
109-
endEvent = data.event = _.animationEndEvent
110-
onEnd = data.callback = function transitionCb (e) {
111-
if (e.target === el) {
112-
_.off(el, endEvent, onEnd)
113-
data.event = data.callback = null
114-
classList.remove(enterClass)
115-
if (cb) cb()
116-
}
117-
}
118-
_.on(el, endEvent, onEnd)
119-
} else {
120-
// no transition applicable
121-
classList.remove(enterClass)
122-
if (cb) cb()
123-
}
124-
184+
push(el, direction, null, enterClass, cb)
125185
} else { // leave
126-
127186
classList.add(leaveClass)
128-
transitionType = getTransitionType(el, data, leaveClass)
129-
if (transitionType) {
130-
endEvent = data.event = transitionType === 1
131-
? _.transitionEndEvent
132-
: _.animationEndEvent
133-
onEnd = data.callback = function transitionCb (e) {
134-
if (e.target === el) {
135-
_.off(el, endEvent, onEnd)
136-
data.event = data.callback = null
137-
// actually remove node here
138-
op()
139-
classList.remove(leaveClass)
140-
if (cb) cb()
141-
}
142-
}
143-
_.on(el, endEvent, onEnd)
144-
} else {
145-
op()
146-
classList.remove(leaveClass)
147-
if (cb) cb()
148-
}
149-
187+
push(el, direction, op, leaveClass, cb)
150188
}
151189
}

test/unit/specs/transition_spec.js

Lines changed: 72 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -149,30 +149,40 @@ if (_.inBrowser && !_.isIE9) {
149149
document.body.removeChild(el)
150150
})
151151

152-
it('skip on 0s duration', function () {
152+
it('skip on 0s duration (execute right at next frame)', function (done) {
153153
el.__v_trans.id = 'test'
154154
el.style.transition =
155155
el.style.WebkitTransition = 'opacity 0s ease'
156156
transition.apply(el, 1, op, vm, cb)
157-
expect(op).toHaveBeenCalled()
158-
expect(cb).toHaveBeenCalled()
159-
expect(el.classList.contains('test-enter')).toBe(false)
160-
transition.apply(el, -1, op, vm, cb)
161-
expect(op.calls.count()).toBe(2)
162-
expect(cb.calls.count()).toBe(2)
163-
expect(el.classList.contains('test-leave')).toBe(false)
157+
_.nextTick(function () {
158+
expect(op).toHaveBeenCalled()
159+
expect(cb).toHaveBeenCalled()
160+
expect(el.classList.contains('test-enter')).toBe(false)
161+
transition.apply(el, -1, op, vm, cb)
162+
_.nextTick(function () {
163+
expect(op.calls.count()).toBe(2)
164+
expect(cb.calls.count()).toBe(2)
165+
expect(el.classList.contains('test-leave')).toBe(false)
166+
done()
167+
})
168+
})
164169
})
165170

166-
it('skip when no transition available', function () {
171+
it('skip when no transition available', function (done) {
167172
el.__v_trans.id = 'test-no-trans'
168173
transition.apply(el, 1, op, vm, cb)
169-
expect(op).toHaveBeenCalled()
170-
expect(cb).toHaveBeenCalled()
171-
expect(el.classList.contains('test-no-trans-enter')).toBe(false)
172-
transition.apply(el, -1, op, vm, cb)
173-
expect(op.calls.count()).toBe(2)
174-
expect(cb.calls.count()).toBe(2)
175-
expect(el.classList.contains('test-no-trans-leave')).toBe(false)
174+
_.nextTick(function () {
175+
expect(op).toHaveBeenCalled()
176+
expect(cb).toHaveBeenCalled()
177+
expect(el.classList.contains('test-no-trans-enter')).toBe(false)
178+
transition.apply(el, -1, op, vm, cb)
179+
_.nextTick(function () {
180+
expect(op.calls.count()).toBe(2)
181+
expect(cb.calls.count()).toBe(2)
182+
expect(el.classList.contains('test-no-trans-leave')).toBe(false)
183+
done()
184+
})
185+
})
176186
})
177187

178188
it('transition enter', function (done) {
@@ -200,19 +210,21 @@ if (_.inBrowser && !_.isIE9) {
200210
el.__v_trans.id = 'test'
201211
// cascaded class style
202212
el.classList.add('test')
203-
// wait a frame: Chrome Android 37 doesn't trigger
204-
// transition if we apply the leave class in the
205-
// same frame.
213+
// wait a tick before applying the transition
214+
// because doing so in the same frame won't trigger
215+
// transition
206216
_.nextTick(function () {
207217
transition.apply(el, -1, op, vm, cb)
208-
expect(op).not.toHaveBeenCalled()
209-
expect(cb).not.toHaveBeenCalled()
210-
expect(el.classList.contains('test-leave')).toBe(true)
211-
_.on(el, _.transitionEndEvent, function () {
212-
expect(op).toHaveBeenCalled()
213-
expect(cb).toHaveBeenCalled()
214-
expect(el.classList.contains('test-leave')).toBe(false)
215-
done()
218+
_.nextTick(function () {
219+
expect(op).not.toHaveBeenCalled()
220+
expect(cb).not.toHaveBeenCalled()
221+
expect(el.classList.contains('test-leave')).toBe(true)
222+
_.on(el, _.transitionEndEvent, function () {
223+
expect(op).toHaveBeenCalled()
224+
expect(cb).toHaveBeenCalled()
225+
expect(el.classList.contains('test-leave')).toBe(false)
226+
done()
227+
})
216228
})
217229
})
218230
})
@@ -224,27 +236,31 @@ if (_.inBrowser && !_.isIE9) {
224236
document.body.appendChild(el)
225237
op()
226238
}, vm, cb)
227-
expect(op).toHaveBeenCalled()
228-
expect(cb).not.toHaveBeenCalled()
229-
expect(el.classList.contains('test-anim-enter')).toBe(true)
230-
_.on(el, _.animationEndEvent, function () {
231-
expect(el.classList.contains('test-anim-enter')).toBe(false)
232-
expect(cb).toHaveBeenCalled()
233-
done()
239+
_.nextTick(function () {
240+
expect(op).toHaveBeenCalled()
241+
expect(cb).not.toHaveBeenCalled()
242+
expect(el.classList.contains('test-anim-enter')).toBe(true)
243+
_.on(el, _.animationEndEvent, function () {
244+
expect(el.classList.contains('test-anim-enter')).toBe(false)
245+
expect(cb).toHaveBeenCalled()
246+
done()
247+
})
234248
})
235249
})
236250

237251
it('animation leave', function (done) {
238252
el.__v_trans.id = 'test-anim'
239253
transition.apply(el, -1, op, vm, cb)
240-
expect(op).not.toHaveBeenCalled()
241-
expect(cb).not.toHaveBeenCalled()
242-
expect(el.classList.contains('test-anim-leave')).toBe(true)
243-
_.on(el, _.animationEndEvent, function () {
244-
expect(op).toHaveBeenCalled()
245-
expect(cb).toHaveBeenCalled()
246-
expect(el.classList.contains('test-anim-leave')).toBe(false)
247-
done()
254+
_.nextTick(function () {
255+
expect(op).not.toHaveBeenCalled()
256+
expect(cb).not.toHaveBeenCalled()
257+
expect(el.classList.contains('test-anim-leave')).toBe(true)
258+
_.on(el, _.animationEndEvent, function () {
259+
expect(op).toHaveBeenCalled()
260+
expect(cb).toHaveBeenCalled()
261+
expect(el.classList.contains('test-anim-leave')).toBe(false)
262+
done()
263+
})
248264
})
249265
})
250266

@@ -254,27 +270,35 @@ if (_.inBrowser && !_.isIE9) {
254270
transition.apply(el, -1, function () {
255271
document.body.removeChild(el)
256272
}, vm, cb)
257-
expect(el.__v_trans.callback).toBeTruthy()
258273
// cancel early
259274
_.nextTick(function () {
275+
expect(el.__v_trans.callback).toBeTruthy()
260276
expect(el.classList.contains('test-leave')).toBe(true)
261277
transition.apply(el, 1, function () {
262278
document.body.appendChild(el)
263279
}, vm)
264280
expect(cb).not.toHaveBeenCalled()
265281
expect(el.classList.contains('test-leave')).toBe(false)
266282
expect(el.__v_trans.callback).toBeNull()
267-
done()
283+
// IMPORTANT
284+
// Let the queue flush finish before enter the next
285+
// test. Don't remove the nextTick.
286+
_.nextTick(done)
268287
})
269288
})
270289

271-
it('cache transition sniff results', function () {
290+
it('cache transition sniff results', function (done) {
272291
el.__v_trans.id = 'test'
273292
el.classList.add('test')
274293
transition.apply(el, 1, op, vm)
275-
expect(window.getComputedStyle.calls.count()).toBe(1)
276-
transition.apply(el, 1, op, vm)
277-
expect(window.getComputedStyle.calls.count()).toBe(1)
294+
_.nextTick(function () {
295+
expect(window.getComputedStyle.calls.count()).toBe(1)
296+
transition.apply(el, 1, op, vm)
297+
_.nextTick(function () {
298+
expect(window.getComputedStyle.calls.count()).toBe(1)
299+
done()
300+
})
301+
})
278302
})
279303

280304
})

0 commit comments

Comments
 (0)