Skip to content

Commit 531cea5

Browse files
committed
perf: avoid unnecessary re-renders when computed property value did not change
close #7767
1 parent 9084747 commit 531cea5

File tree

4 files changed

+153
-51
lines changed

4 files changed

+153
-51
lines changed

src/core/instance/state.js

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import config from '../config'
44
import Watcher from '../observer/watcher'
5-
import Dep, { pushTarget, popTarget } from '../observer/dep'
5+
import { pushTarget, popTarget } from '../observer/dep'
66
import { isUpdatingChildComponent } from './lifecycle'
77

88
import {
@@ -164,7 +164,7 @@ export function getData (data: Function, vm: Component): any {
164164
}
165165
}
166166

167-
const computedWatcherOptions = { lazy: true }
167+
const computedWatcherOptions = { computed: true }
168168

169169
function initComputed (vm: Component, computed: Object) {
170170
// $flow-disable-line
@@ -244,13 +244,8 @@ function createComputedGetter (key) {
244244
return function computedGetter () {
245245
const watcher = this._computedWatchers && this._computedWatchers[key]
246246
if (watcher) {
247-
if (watcher.dirty) {
248-
watcher.evaluate()
249-
}
250-
if (Dep.target) {
251-
watcher.depend()
252-
}
253-
return watcher.value
247+
watcher.depend()
248+
return watcher.evaluate()
254249
}
255250
}
256251
}

src/core/observer/watcher.js

Lines changed: 64 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,11 @@ export default class Watcher {
2929
id: number;
3030
deep: boolean;
3131
user: boolean;
32-
lazy: boolean;
32+
computed: boolean;
3333
sync: boolean;
3434
dirty: boolean;
3535
active: boolean;
36+
dep: Dep;
3637
deps: Array<Dep>;
3738
newDeps: Array<Dep>;
3839
depIds: SimpleSet;
@@ -56,15 +57,15 @@ export default class Watcher {
5657
if (options) {
5758
this.deep = !!options.deep
5859
this.user = !!options.user
59-
this.lazy = !!options.lazy
60+
this.computed = !!options.computed
6061
this.sync = !!options.sync
6162
} else {
62-
this.deep = this.user = this.lazy = this.sync = false
63+
this.deep = this.user = this.computed = this.sync = false
6364
}
6465
this.cb = cb
6566
this.id = ++uid // uid for batching
6667
this.active = true
67-
this.dirty = this.lazy // for lazy watchers
68+
this.dirty = this.computed // for computed watchers
6869
this.deps = []
6970
this.newDeps = []
7071
this.depIds = new Set()
@@ -87,9 +88,12 @@ export default class Watcher {
8788
)
8889
}
8990
}
90-
this.value = this.lazy
91-
? undefined
92-
: this.get()
91+
if (this.computed) {
92+
this.value = undefined
93+
this.dep = new Dep()
94+
} else {
95+
this.value = this.get()
96+
}
9397
}
9498

9599
/**
@@ -160,8 +164,24 @@ export default class Watcher {
160164
*/
161165
update () {
162166
/* istanbul ignore else */
163-
if (this.lazy) {
164-
this.dirty = true
167+
if (this.computed) {
168+
// A computed property watcher has two modes: lazy and activated.
169+
// It initializes as lazy by default, and only becomes activated when
170+
// it is depended on by at least one subscriber, which is typically
171+
// another computed property or a component's render function.
172+
if (this.dep.subs.length === 0) {
173+
// In lazy mode, we don't want to perform computations until necessary,
174+
// so we simply mark the watcher as dirty. The actual computation is
175+
// performed just-in-time in this.evaluate() when the computed property
176+
// is accessed.
177+
this.dirty = true
178+
} else {
179+
// In activated mode, we want to proactively perform the computation
180+
// but only notify our subscribers when the value has indeed changed.
181+
this.getAndInvoke(() => {
182+
this.dep.notify()
183+
})
184+
}
165185
} else if (this.sync) {
166186
this.run()
167187
} else {
@@ -175,47 +195,54 @@ export default class Watcher {
175195
*/
176196
run () {
177197
if (this.active) {
178-
const value = this.get()
179-
if (
180-
value !== this.value ||
181-
// Deep watchers and watchers on Object/Arrays should fire even
182-
// when the value is the same, because the value may
183-
// have mutated.
184-
isObject(value) ||
185-
this.deep
186-
) {
187-
// set new value
188-
const oldValue = this.value
189-
this.value = value
190-
if (this.user) {
191-
try {
192-
this.cb.call(this.vm, value, oldValue)
193-
} catch (e) {
194-
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
195-
}
196-
} else {
197-
this.cb.call(this.vm, value, oldValue)
198+
this.getAndInvoke(this.cb)
199+
}
200+
}
201+
202+
getAndInvoke (cb: Function) {
203+
const value = this.get()
204+
if (
205+
value !== this.value ||
206+
// Deep watchers and watchers on Object/Arrays should fire even
207+
// when the value is the same, because the value may
208+
// have mutated.
209+
isObject(value) ||
210+
this.deep
211+
) {
212+
// set new value
213+
const oldValue = this.value
214+
this.value = value
215+
this.dirty = false
216+
if (this.user) {
217+
try {
218+
cb.call(this.vm, value, oldValue)
219+
} catch (e) {
220+
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
198221
}
222+
} else {
223+
cb.call(this.vm, value, oldValue)
199224
}
200225
}
201226
}
202227

203228
/**
204-
* Evaluate the value of the watcher.
205-
* This only gets called for lazy watchers.
229+
* Evaluate and return the value of the watcher.
230+
* This only gets called for computed property watchers.
206231
*/
207232
evaluate () {
208-
this.value = this.get()
209-
this.dirty = false
233+
if (this.dirty) {
234+
this.value = this.get()
235+
this.dirty = false
236+
}
237+
return this.value
210238
}
211239

212240
/**
213-
* Depend on all deps collected by this watcher.
241+
* Depend on this watcher. Only for computed property watchers.
214242
*/
215243
depend () {
216-
let i = this.deps.length
217-
while (i--) {
218-
this.deps[i].depend()
244+
if (this.dep && Dep.target) {
245+
this.dep.depend()
219246
}
220247
}
221248

test/unit/features/options/computed.spec.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,4 +216,40 @@ describe('Options computed', () => {
216216
})
217217
expect(() => vm.a).toThrowError('rethrow')
218218
})
219+
220+
// #7767
221+
it('should avoid unnecessary re-renders', done => {
222+
const computedSpy = jasmine.createSpy('computed')
223+
const updatedSpy = jasmine.createSpy('updated')
224+
const vm = new Vue({
225+
data: {
226+
msg: 'bar'
227+
},
228+
computed: {
229+
a () {
230+
computedSpy()
231+
return this.msg !== 'foo'
232+
}
233+
},
234+
template: `<div>{{ a }}</div>`,
235+
updated: updatedSpy
236+
}).$mount()
237+
238+
expect(vm.$el.textContent).toBe('true')
239+
expect(computedSpy.calls.count()).toBe(1)
240+
expect(updatedSpy.calls.count()).toBe(0)
241+
242+
vm.msg = 'baz'
243+
waitForUpdate(() => {
244+
expect(vm.$el.textContent).toBe('true')
245+
expect(computedSpy.calls.count()).toBe(2)
246+
expect(updatedSpy.calls.count()).toBe(0)
247+
}).then(() => {
248+
vm.msg = 'foo'
249+
}).then(() => {
250+
expect(vm.$el.textContent).toBe('false')
251+
expect(computedSpy.calls.count()).toBe(3)
252+
expect(updatedSpy.calls.count()).toBe(1)
253+
}).then(done)
254+
})
219255
})

test/unit/modules/observer/watcher.spec.js

Lines changed: 49 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -144,26 +144,70 @@ describe('Watcher', () => {
144144
}).then(done)
145145
})
146146

147-
it('lazy mode', done => {
147+
it('computed mode, lazy', done => {
148+
let getterCallCount = 0
148149
const watcher = new Watcher(vm, function () {
150+
getterCallCount++
149151
return this.a + this.b.d
150-
}, null, { lazy: true })
151-
expect(watcher.lazy).toBe(true)
152+
}, null, { computed: true })
153+
154+
expect(getterCallCount).toBe(0)
155+
expect(watcher.computed).toBe(true)
152156
expect(watcher.value).toBeUndefined()
153157
expect(watcher.dirty).toBe(true)
154-
watcher.evaluate()
158+
expect(watcher.dep).toBeTruthy()
159+
160+
const value = watcher.evaluate()
161+
expect(getterCallCount).toBe(1)
162+
expect(value).toBe(5)
155163
expect(watcher.value).toBe(5)
156164
expect(watcher.dirty).toBe(false)
165+
166+
// should not get again if not dirty
167+
watcher.evaluate()
168+
expect(getterCallCount).toBe(1)
169+
157170
vm.a = 2
158171
waitForUpdate(() => {
172+
expect(getterCallCount).toBe(1)
159173
expect(watcher.value).toBe(5)
160174
expect(watcher.dirty).toBe(true)
161-
watcher.evaluate()
175+
176+
const value = watcher.evaluate()
177+
expect(getterCallCount).toBe(2)
178+
expect(value).toBe(6)
162179
expect(watcher.value).toBe(6)
163180
expect(watcher.dirty).toBe(false)
164181
}).then(done)
165182
})
166183

184+
it('computed mode, activated', done => {
185+
let getterCallCount = 0
186+
const watcher = new Watcher(vm, function () {
187+
getterCallCount++
188+
return this.a + this.b.d
189+
}, null, { computed: true })
190+
191+
// activate by mocking a subscriber
192+
const subMock = jasmine.createSpyObj('sub', ['update'])
193+
watcher.dep.addSub(subMock)
194+
195+
const value = watcher.evaluate()
196+
expect(getterCallCount).toBe(1)
197+
expect(value).toBe(5)
198+
199+
vm.a = 2
200+
waitForUpdate(() => {
201+
expect(getterCallCount).toBe(2)
202+
expect(subMock.update).toHaveBeenCalled()
203+
204+
// since already computed, calling evaluate again should not trigger
205+
// getter
206+
watcher.evaluate()
207+
expect(getterCallCount).toBe(2)
208+
}).then(done)
209+
})
210+
167211
it('teardown', done => {
168212
const watcher = new Watcher(vm, 'b.c', spy)
169213
watcher.teardown()

0 commit comments

Comments
 (0)