mirror of
https://github.com/vuejs/vue.git
synced 2024-11-22 04:39:46 +00:00
perf: avoid unnecessary re-renders when computed property value did not change (#7824)
close #7767
This commit is contained in:
parent
f43ce3a5d8
commit
653aac2c57
@ -2,7 +2,7 @@
|
||||
|
||||
import config from '../config'
|
||||
import Watcher from '../observer/watcher'
|
||||
import Dep, { pushTarget, popTarget } from '../observer/dep'
|
||||
import { pushTarget, popTarget } from '../observer/dep'
|
||||
import { isUpdatingChildComponent } from './lifecycle'
|
||||
|
||||
import {
|
||||
@ -164,7 +164,7 @@ export function getData (data: Function, vm: Component): any {
|
||||
}
|
||||
}
|
||||
|
||||
const computedWatcherOptions = { lazy: true }
|
||||
const computedWatcherOptions = { computed: true }
|
||||
|
||||
function initComputed (vm: Component, computed: Object) {
|
||||
// $flow-disable-line
|
||||
@ -244,13 +244,8 @@ function createComputedGetter (key) {
|
||||
return function computedGetter () {
|
||||
const watcher = this._computedWatchers && this._computedWatchers[key]
|
||||
if (watcher) {
|
||||
if (watcher.dirty) {
|
||||
watcher.evaluate()
|
||||
}
|
||||
if (Dep.target) {
|
||||
watcher.depend()
|
||||
}
|
||||
return watcher.value
|
||||
return watcher.evaluate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -29,10 +29,11 @@ export default class Watcher {
|
||||
id: number;
|
||||
deep: boolean;
|
||||
user: boolean;
|
||||
lazy: boolean;
|
||||
computed: boolean;
|
||||
sync: boolean;
|
||||
dirty: boolean;
|
||||
active: boolean;
|
||||
dep: Dep;
|
||||
deps: Array<Dep>;
|
||||
newDeps: Array<Dep>;
|
||||
depIds: SimpleSet;
|
||||
@ -57,16 +58,16 @@ export default class Watcher {
|
||||
if (options) {
|
||||
this.deep = !!options.deep
|
||||
this.user = !!options.user
|
||||
this.lazy = !!options.lazy
|
||||
this.computed = !!options.computed
|
||||
this.sync = !!options.sync
|
||||
this.before = options.before
|
||||
} else {
|
||||
this.deep = this.user = this.lazy = this.sync = false
|
||||
this.deep = this.user = this.computed = this.sync = false
|
||||
}
|
||||
this.cb = cb
|
||||
this.id = ++uid // uid for batching
|
||||
this.active = true
|
||||
this.dirty = this.lazy // for lazy watchers
|
||||
this.dirty = this.computed // for computed watchers
|
||||
this.deps = []
|
||||
this.newDeps = []
|
||||
this.depIds = new Set()
|
||||
@ -89,9 +90,12 @@ export default class Watcher {
|
||||
)
|
||||
}
|
||||
}
|
||||
this.value = this.lazy
|
||||
? undefined
|
||||
: this.get()
|
||||
if (this.computed) {
|
||||
this.value = undefined
|
||||
this.dep = new Dep()
|
||||
} else {
|
||||
this.value = this.get()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -162,8 +166,24 @@ export default class Watcher {
|
||||
*/
|
||||
update () {
|
||||
/* istanbul ignore else */
|
||||
if (this.lazy) {
|
||||
if (this.computed) {
|
||||
// A computed property watcher has two modes: lazy and activated.
|
||||
// It initializes as lazy by default, and only becomes activated when
|
||||
// it is depended on by at least one subscriber, which is typically
|
||||
// another computed property or a component's render function.
|
||||
if (this.dep.subs.length === 0) {
|
||||
// In lazy mode, we don't want to perform computations until necessary,
|
||||
// so we simply mark the watcher as dirty. The actual computation is
|
||||
// performed just-in-time in this.evaluate() when the computed property
|
||||
// is accessed.
|
||||
this.dirty = true
|
||||
} else {
|
||||
// In activated mode, we want to proactively perform the computation
|
||||
// but only notify our subscribers when the value has indeed changed.
|
||||
this.getAndInvoke(() => {
|
||||
this.dep.notify()
|
||||
})
|
||||
}
|
||||
} else if (this.sync) {
|
||||
this.run()
|
||||
} else {
|
||||
@ -177,6 +197,11 @@ export default class Watcher {
|
||||
*/
|
||||
run () {
|
||||
if (this.active) {
|
||||
this.getAndInvoke(this.cb)
|
||||
}
|
||||
}
|
||||
|
||||
getAndInvoke (cb: Function) {
|
||||
const value = this.get()
|
||||
if (
|
||||
value !== this.value ||
|
||||
@ -189,35 +214,37 @@ export default class Watcher {
|
||||
// set new value
|
||||
const oldValue = this.value
|
||||
this.value = value
|
||||
this.dirty = false
|
||||
if (this.user) {
|
||||
try {
|
||||
this.cb.call(this.vm, value, oldValue)
|
||||
cb.call(this.vm, value, oldValue)
|
||||
} catch (e) {
|
||||
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
|
||||
}
|
||||
} else {
|
||||
this.cb.call(this.vm, value, oldValue)
|
||||
}
|
||||
cb.call(this.vm, value, oldValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate the value of the watcher.
|
||||
* This only gets called for lazy watchers.
|
||||
* Evaluate and return the value of the watcher.
|
||||
* This only gets called for computed property watchers.
|
||||
*/
|
||||
evaluate () {
|
||||
if (this.dirty) {
|
||||
this.value = this.get()
|
||||
this.dirty = false
|
||||
}
|
||||
return this.value
|
||||
}
|
||||
|
||||
/**
|
||||
* Depend on all deps collected by this watcher.
|
||||
* Depend on this watcher. Only for computed property watchers.
|
||||
*/
|
||||
depend () {
|
||||
let i = this.deps.length
|
||||
while (i--) {
|
||||
this.deps[i].depend()
|
||||
if (this.dep && Dep.target) {
|
||||
this.dep.depend()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -216,4 +216,40 @@ describe('Options computed', () => {
|
||||
})
|
||||
expect(() => vm.a).toThrowError('rethrow')
|
||||
})
|
||||
|
||||
// #7767
|
||||
it('should avoid unnecessary re-renders', done => {
|
||||
const computedSpy = jasmine.createSpy('computed')
|
||||
const updatedSpy = jasmine.createSpy('updated')
|
||||
const vm = new Vue({
|
||||
data: {
|
||||
msg: 'bar'
|
||||
},
|
||||
computed: {
|
||||
a () {
|
||||
computedSpy()
|
||||
return this.msg !== 'foo'
|
||||
}
|
||||
},
|
||||
template: `<div>{{ a }}</div>`,
|
||||
updated: updatedSpy
|
||||
}).$mount()
|
||||
|
||||
expect(vm.$el.textContent).toBe('true')
|
||||
expect(computedSpy.calls.count()).toBe(1)
|
||||
expect(updatedSpy.calls.count()).toBe(0)
|
||||
|
||||
vm.msg = 'baz'
|
||||
waitForUpdate(() => {
|
||||
expect(vm.$el.textContent).toBe('true')
|
||||
expect(computedSpy.calls.count()).toBe(2)
|
||||
expect(updatedSpy.calls.count()).toBe(0)
|
||||
}).then(() => {
|
||||
vm.msg = 'foo'
|
||||
}).then(() => {
|
||||
expect(vm.$el.textContent).toBe('false')
|
||||
expect(computedSpy.calls.count()).toBe(3)
|
||||
expect(updatedSpy.calls.count()).toBe(1)
|
||||
}).then(done)
|
||||
})
|
||||
})
|
||||
|
@ -144,26 +144,70 @@ describe('Watcher', () => {
|
||||
}).then(done)
|
||||
})
|
||||
|
||||
it('lazy mode', done => {
|
||||
it('computed mode, lazy', done => {
|
||||
let getterCallCount = 0
|
||||
const watcher = new Watcher(vm, function () {
|
||||
getterCallCount++
|
||||
return this.a + this.b.d
|
||||
}, null, { lazy: true })
|
||||
expect(watcher.lazy).toBe(true)
|
||||
}, null, { computed: true })
|
||||
|
||||
expect(getterCallCount).toBe(0)
|
||||
expect(watcher.computed).toBe(true)
|
||||
expect(watcher.value).toBeUndefined()
|
||||
expect(watcher.dirty).toBe(true)
|
||||
watcher.evaluate()
|
||||
expect(watcher.dep).toBeTruthy()
|
||||
|
||||
const value = watcher.evaluate()
|
||||
expect(getterCallCount).toBe(1)
|
||||
expect(value).toBe(5)
|
||||
expect(watcher.value).toBe(5)
|
||||
expect(watcher.dirty).toBe(false)
|
||||
|
||||
// should not get again if not dirty
|
||||
watcher.evaluate()
|
||||
expect(getterCallCount).toBe(1)
|
||||
|
||||
vm.a = 2
|
||||
waitForUpdate(() => {
|
||||
expect(getterCallCount).toBe(1)
|
||||
expect(watcher.value).toBe(5)
|
||||
expect(watcher.dirty).toBe(true)
|
||||
watcher.evaluate()
|
||||
|
||||
const value = watcher.evaluate()
|
||||
expect(getterCallCount).toBe(2)
|
||||
expect(value).toBe(6)
|
||||
expect(watcher.value).toBe(6)
|
||||
expect(watcher.dirty).toBe(false)
|
||||
}).then(done)
|
||||
})
|
||||
|
||||
it('computed mode, activated', done => {
|
||||
let getterCallCount = 0
|
||||
const watcher = new Watcher(vm, function () {
|
||||
getterCallCount++
|
||||
return this.a + this.b.d
|
||||
}, null, { computed: true })
|
||||
|
||||
// activate by mocking a subscriber
|
||||
const subMock = jasmine.createSpyObj('sub', ['update'])
|
||||
watcher.dep.addSub(subMock)
|
||||
|
||||
const value = watcher.evaluate()
|
||||
expect(getterCallCount).toBe(1)
|
||||
expect(value).toBe(5)
|
||||
|
||||
vm.a = 2
|
||||
waitForUpdate(() => {
|
||||
expect(getterCallCount).toBe(2)
|
||||
expect(subMock.update).toHaveBeenCalled()
|
||||
|
||||
// since already computed, calling evaluate again should not trigger
|
||||
// getter
|
||||
watcher.evaluate()
|
||||
expect(getterCallCount).toBe(2)
|
||||
}).then(done)
|
||||
})
|
||||
|
||||
it('teardown', done => {
|
||||
const watcher = new Watcher(vm, 'b.c', spy)
|
||||
watcher.teardown()
|
||||
|
Loading…
Reference in New Issue
Block a user