Skip to content

Commit 9482e51

Browse files
committed
v-repeat optimization
- When the array is swapped, will reuse existing VM for any element present in the old array. This greatly improves performance when the repeated VMs have a complicated nested structure themselves. - Fixed issue when array is swapped new length is not emitted - Fixed v-if not removing its reference comment node when unbound - Fixed transition when element is invisible the transitionend callback is never fired resulting in element stuck in DOM
1 parent 8dc9954 commit 9482e51

File tree

8 files changed

+161
-45
lines changed

8 files changed

+161
-45
lines changed

src/directives/if.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,5 +53,9 @@ module.exports = {
5353

5454
unbind: function () {
5555
this.el.vue_ref = null
56+
var ref = this.ref
57+
if (ref.parentNode) {
58+
ref.parentNode.removeChild(ref)
59+
}
5660
}
5761
}

src/directives/repeat.js

Lines changed: 66 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,12 @@ module.exports = {
132132
this.buildItem()
133133
this.initiated = true
134134
}
135+
136+
// keep reference of old data and VMs
137+
// so we can reuse them if possible
138+
this.old = this.collection
139+
var oldVMs = this.oldVMs = this.vms
140+
135141
collection = this.collection = collection || []
136142
this.vms = []
137143
if (this.childId) {
@@ -143,11 +149,25 @@ module.exports = {
143149
if (!collection.__observer__) Observer.watchArray(collection)
144150
collection.__observer__.on('mutate', this.mutationListener)
145151

146-
// create child-vms and append to DOM
152+
// create new VMs and append to DOM
147153
if (collection.length) {
148154
collection.forEach(this.buildItem, this)
149155
if (!init) this.changed()
150156
}
157+
158+
// destroy unused old VMs
159+
if (oldVMs) {
160+
var i = oldVMs.length, vm
161+
while (i--) {
162+
vm = oldVMs[i]
163+
if (vm.$reused) {
164+
vm.$reused = false
165+
} else {
166+
vm.$destroy()
167+
}
168+
}
169+
}
170+
this.old = this.oldVMs = null
151171
},
152172

153173
/**
@@ -174,33 +194,58 @@ module.exports = {
174194
*/
175195
buildItem: function (data, index) {
176196

177-
var el = this.el.cloneNode(true),
178-
ctn = this.container,
197+
var ctn = this.container,
179198
vms = this.vms,
180199
col = this.collection,
181-
ref, item, primitive
200+
el, i, ref, item, primitive, noInsert
182201

183202
// append node into DOM first
184203
// so v-if can get access to parentNode
185204
if (data) {
205+
206+
if (this.old) {
207+
i = this.old.indexOf(data)
208+
}
209+
210+
if (i > -1) { // existing, reuse the old VM
211+
212+
item = this.oldVMs[i]
213+
// mark, so it won't be destroyed
214+
item.$reused = true
215+
el = item.$el
216+
// don't forget to update index
217+
data.$index = index
218+
// existing VM's el can possibly be detached by v-if.
219+
// in that case don't insert.
220+
noInsert = !el.parentNode
221+
222+
} else { // new data, need to create new VM
223+
224+
el = this.el.cloneNode(true)
225+
// process transition info before appending
226+
el.vue_trans = utils.attr(el, 'transition', true)
227+
// wrap primitive element in an object
228+
if (utils.typeOf(data) !== 'Object') {
229+
primitive = true
230+
data = { value: data }
231+
}
232+
233+
}
234+
186235
ref = vms.length > index
187236
? vms[index].$el
188237
: this.ref
189238
// make sure it works with v-if
190239
if (!ref.parentNode) ref = ref.vue_ref
191-
// process transition info before appending
192-
el.vue_trans = utils.attr(el, 'transition', true)
193-
transition(el, 1, function () {
194-
ctn.insertBefore(el, ref)
195-
}, this.compiler)
196-
// wrap primitive element in an object
197-
if (utils.typeOf(data) !== 'Object') {
198-
primitive = true
199-
data = { value: data }
240+
// insert node with transition
241+
if (!noInsert) {
242+
transition(el, 1, function () {
243+
ctn.insertBefore(el, ref)
244+
}, this.compiler)
200245
}
201246
}
202247

203-
item = new this.Ctor({
248+
item = item || new this.Ctor({
204249
el: el,
205250
data: data,
206251
compilerOptions: {
@@ -228,15 +273,17 @@ module.exports = {
228273
}
229274
},
230275

231-
reset: function () {
276+
reset: function (destroyAll) {
232277
if (this.childId) {
233278
delete this.vm.$[this.childId]
234279
}
235280
if (this.collection) {
236281
this.collection.__observer__.off('mutate', this.mutationListener)
237-
var i = this.vms.length
238-
while (i--) {
239-
this.vms[i].$destroy()
282+
if (destroyAll) {
283+
var i = this.vms.length
284+
while (i--) {
285+
this.vms[i].$destroy()
286+
}
240287
}
241288
}
242289
var ctn = this.container,
@@ -248,6 +295,6 @@ module.exports = {
248295
},
249296

250297
unbind: function () {
251-
this.reset()
298+
this.reset(true)
252299
}
253300
}

src/observer.js

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -138,12 +138,10 @@ function convert (obj, key) {
138138
// this means when an object is observed it will emit
139139
// a first batch of set events.
140140
var observer = obj.__observer__,
141-
values = observer.values,
142-
val = values[key] = obj[key]
143-
observer.emit('set', key, val)
144-
if (Array.isArray(val)) {
145-
observer.emit('set', key + '.length', val.length)
146-
}
141+
values = observer.values
142+
143+
init(obj[key])
144+
147145
Object.defineProperty(obj, key, {
148146
get: function () {
149147
var value = values[key]
@@ -156,15 +154,21 @@ function convert (obj, key) {
156154
set: function (newVal) {
157155
var oldVal = values[key]
158156
unobserve(oldVal, key, observer)
159-
values[key] = newVal
160157
copyPaths(newVal, oldVal)
161158
// an immediate property should notify its parent
162159
// to emit set for itself too
163-
observer.emit('set', key, newVal, true)
164-
observe(newVal, key, observer)
160+
init(newVal, true)
165161
}
166162
})
167-
observe(val, key, observer)
163+
164+
function init (val, propagate) {
165+
values[key] = val
166+
observer.emit('set', key, val, propagate)
167+
if (Array.isArray(val)) {
168+
observer.emit('set', key + '.length', val.length)
169+
}
170+
observe(val, key, observer)
171+
}
168172
}
169173

170174
/**

src/transition.js

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -92,20 +92,25 @@ function applyTransitionClass (el, stage, changeState) {
9292

9393
} else { // leave
9494

95-
// trigger hide transition
96-
classList.add(config.leaveClass)
97-
var onEnd = function (e) {
98-
if (e.target === el) {
99-
el.removeEventListener(endEvent, onEnd)
100-
el.vue_trans_cb = null
101-
// actually remove node here
102-
changeState()
103-
classList.remove(config.leaveClass)
95+
if (el.offsetWidth || el.offsetHeight) {
96+
// trigger hide transition
97+
classList.add(config.leaveClass)
98+
var onEnd = function (e) {
99+
if (e.target === el) {
100+
el.removeEventListener(endEvent, onEnd)
101+
el.vue_trans_cb = null
102+
// actually remove node here
103+
changeState()
104+
classList.remove(config.leaveClass)
105+
}
104106
}
107+
// attach transition end listener
108+
el.addEventListener(endEvent, onEnd)
109+
el.vue_trans_cb = onEnd
110+
} else {
111+
// directly remove invisible elements
112+
changeState()
105113
}
106-
// attach transition end listener
107-
el.addEventListener(endEvent, onEnd)
108-
el.vue_trans_cb = onEnd
109114
return codes.CSS_L
110115

111116
}

test/functional/fixtures/repeated-items.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
</p>
1313
<p>Total items: <span class="count" v-text="items.length"></span></p>
1414
<ul>
15-
<li class="item" v-repeat="items">
15+
<li class="item" v-repeat="items" v-ref="items">
1616
{{$index}} {{title}}
1717
</li>
1818
</ul>

test/functional/specs/repeated-items.js

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
casper.test.begin('Repeated Items', 41, function (test) {
1+
/* global demo */
2+
3+
casper.test.begin('Repeated Items', 50, function (test) {
24

35
casper
46
.start('./fixtures/repeated-items.html')
@@ -81,6 +83,37 @@ casper.test.begin('Repeated Items', 41, function (test) {
8183
test.assertSelectorHasText('.item:nth-child(1)', '0 6')
8284
test.assertSelectorHasText('.item:nth-child(2)', '1 7')
8385
})
86+
// test swap entire array
87+
.thenEvaluate(function () {
88+
demo.items = [{title:'A'}, {title:'B'}, {title:'C'}]
89+
})
90+
.then(function () {
91+
test.assertSelectorHasText('.count', '3')
92+
test.assertSelectorHasText('.item:nth-child(1)', '0 A')
93+
test.assertSelectorHasText('.item:nth-child(2)', '1 B')
94+
test.assertSelectorHasText('.item:nth-child(3)', '2 C')
95+
})
96+
// test swap array with old elements
97+
// should reuse existing VMs!
98+
.thenEvaluate(function () {
99+
window.oldVMs = demo.$.items
100+
demo.items = [demo.items[2],demo.items[1],demo.items[0]]
101+
})
102+
.then(function () {
103+
test.assertSelectorHasText('.count', '3')
104+
test.assertSelectorHasText('.item:nth-child(1)', '0 C')
105+
test.assertSelectorHasText('.item:nth-child(2)', '1 B')
106+
test.assertSelectorHasText('.item:nth-child(3)', '2 A')
107+
test.assertEval(function () {
108+
var i = window.oldVMs.length
109+
while (i--) {
110+
if (window.oldVMs[i] !== demo.$.items[2 - i]) {
111+
return false
112+
}
113+
}
114+
return true
115+
})
116+
})
84117
.run(function () {
85118
test.done()
86119
})

test/functional/specs/transition.js

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
casper.test.begin('Transition', 23, function (test) {
1+
casper.test.begin('Transition', 25, function (test) {
22

33
var minWait = 50,
44
transDuration = 200
@@ -50,9 +50,17 @@ casper.test.begin('Transition', 23, function (test) {
5050
})
5151
.thenClick('.splice')
5252
.wait(minWait, function () {
53-
test.assertElementCount('.test', 4)
53+
test.assertElementCount('.test', 3)
5454
test.assertVisible('.test[data-id="99"]')
5555
})
56+
// test Array swapping with transition
57+
.thenEvaluate(function () {
58+
test.items = [test.items[1], {a:3}]
59+
})
60+
.wait(transDuration + minWait, function () {
61+
test.assertElementCount('.test', 3)
62+
test.assertVisible('.test[data-id="3"]')
63+
})
5664
.run(function () {
5765
test.done()
5866
})

test/unit/specs/transition.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,9 +94,24 @@ describe('UNIT: Transition', function () {
9494
var el = mockEl('css'),
9595
c = mockChange(),
9696
compiler = mockCompiler(),
97+
code
98+
99+
before(function () {
100+
document.body.appendChild(el)
101+
})
102+
103+
it('should call change immediately if el is invisible', function () {
104+
var el = mockEl('css'),
105+
c = mockChange(),
106+
compiler = mockCompiler()
97107
code = transition(el, -1, c.change, compiler)
108+
assert.ok(c.called)
109+
assert.ok(compiler.detached)
110+
})
98111

99112
it('should attach an ontransitionend listener', function () {
113+
el.style.width = '1px'
114+
code = transition(el, -1, c.change, compiler)
100115
assert.ok(typeof el.vue_trans_cb === 'function')
101116
})
102117

0 commit comments

Comments
 (0)