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 config from '../config'
|
||||||
import Watcher from '../observer/watcher'
|
import Watcher from '../observer/watcher'
|
||||||
import Dep, { pushTarget, popTarget } from '../observer/dep'
|
import { pushTarget, popTarget } from '../observer/dep'
|
||||||
import { isUpdatingChildComponent } from './lifecycle'
|
import { isUpdatingChildComponent } from './lifecycle'
|
||||||
|
|
||||||
import {
|
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) {
|
function initComputed (vm: Component, computed: Object) {
|
||||||
// $flow-disable-line
|
// $flow-disable-line
|
||||||
@ -244,13 +244,8 @@ function createComputedGetter (key) {
|
|||||||
return function computedGetter () {
|
return function computedGetter () {
|
||||||
const watcher = this._computedWatchers && this._computedWatchers[key]
|
const watcher = this._computedWatchers && this._computedWatchers[key]
|
||||||
if (watcher) {
|
if (watcher) {
|
||||||
if (watcher.dirty) {
|
watcher.depend()
|
||||||
watcher.evaluate()
|
return watcher.evaluate()
|
||||||
}
|
|
||||||
if (Dep.target) {
|
|
||||||
watcher.depend()
|
|
||||||
}
|
|
||||||
return watcher.value
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -29,10 +29,11 @@ export default class Watcher {
|
|||||||
id: number;
|
id: number;
|
||||||
deep: boolean;
|
deep: boolean;
|
||||||
user: boolean;
|
user: boolean;
|
||||||
lazy: boolean;
|
computed: boolean;
|
||||||
sync: boolean;
|
sync: boolean;
|
||||||
dirty: boolean;
|
dirty: boolean;
|
||||||
active: boolean;
|
active: boolean;
|
||||||
|
dep: Dep;
|
||||||
deps: Array<Dep>;
|
deps: Array<Dep>;
|
||||||
newDeps: Array<Dep>;
|
newDeps: Array<Dep>;
|
||||||
depIds: SimpleSet;
|
depIds: SimpleSet;
|
||||||
@ -57,16 +58,16 @@ export default class Watcher {
|
|||||||
if (options) {
|
if (options) {
|
||||||
this.deep = !!options.deep
|
this.deep = !!options.deep
|
||||||
this.user = !!options.user
|
this.user = !!options.user
|
||||||
this.lazy = !!options.lazy
|
this.computed = !!options.computed
|
||||||
this.sync = !!options.sync
|
this.sync = !!options.sync
|
||||||
this.before = options.before
|
this.before = options.before
|
||||||
} else {
|
} else {
|
||||||
this.deep = this.user = this.lazy = this.sync = false
|
this.deep = this.user = this.computed = this.sync = false
|
||||||
}
|
}
|
||||||
this.cb = cb
|
this.cb = cb
|
||||||
this.id = ++uid // uid for batching
|
this.id = ++uid // uid for batching
|
||||||
this.active = true
|
this.active = true
|
||||||
this.dirty = this.lazy // for lazy watchers
|
this.dirty = this.computed // for computed watchers
|
||||||
this.deps = []
|
this.deps = []
|
||||||
this.newDeps = []
|
this.newDeps = []
|
||||||
this.depIds = new Set()
|
this.depIds = new Set()
|
||||||
@ -89,9 +90,12 @@ export default class Watcher {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.value = this.lazy
|
if (this.computed) {
|
||||||
? undefined
|
this.value = undefined
|
||||||
: this.get()
|
this.dep = new Dep()
|
||||||
|
} else {
|
||||||
|
this.value = this.get()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -162,8 +166,24 @@ export default class Watcher {
|
|||||||
*/
|
*/
|
||||||
update () {
|
update () {
|
||||||
/* istanbul ignore else */
|
/* istanbul ignore else */
|
||||||
if (this.lazy) {
|
if (this.computed) {
|
||||||
this.dirty = true
|
// 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) {
|
} else if (this.sync) {
|
||||||
this.run()
|
this.run()
|
||||||
} else {
|
} else {
|
||||||
@ -177,47 +197,54 @@ export default class Watcher {
|
|||||||
*/
|
*/
|
||||||
run () {
|
run () {
|
||||||
if (this.active) {
|
if (this.active) {
|
||||||
const value = this.get()
|
this.getAndInvoke(this.cb)
|
||||||
if (
|
}
|
||||||
value !== this.value ||
|
}
|
||||||
// Deep watchers and watchers on Object/Arrays should fire even
|
|
||||||
// when the value is the same, because the value may
|
getAndInvoke (cb: Function) {
|
||||||
// have mutated.
|
const value = this.get()
|
||||||
isObject(value) ||
|
if (
|
||||||
this.deep
|
value !== this.value ||
|
||||||
) {
|
// Deep watchers and watchers on Object/Arrays should fire even
|
||||||
// set new value
|
// when the value is the same, because the value may
|
||||||
const oldValue = this.value
|
// have mutated.
|
||||||
this.value = value
|
isObject(value) ||
|
||||||
if (this.user) {
|
this.deep
|
||||||
try {
|
) {
|
||||||
this.cb.call(this.vm, value, oldValue)
|
// set new value
|
||||||
} catch (e) {
|
const oldValue = this.value
|
||||||
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
|
this.value = value
|
||||||
}
|
this.dirty = false
|
||||||
} else {
|
if (this.user) {
|
||||||
this.cb.call(this.vm, value, oldValue)
|
try {
|
||||||
|
cb.call(this.vm, value, oldValue)
|
||||||
|
} catch (e) {
|
||||||
|
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
cb.call(this.vm, value, oldValue)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Evaluate the value of the watcher.
|
* Evaluate and return the value of the watcher.
|
||||||
* This only gets called for lazy watchers.
|
* This only gets called for computed property watchers.
|
||||||
*/
|
*/
|
||||||
evaluate () {
|
evaluate () {
|
||||||
this.value = this.get()
|
if (this.dirty) {
|
||||||
this.dirty = false
|
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 () {
|
depend () {
|
||||||
let i = this.deps.length
|
if (this.dep && Dep.target) {
|
||||||
while (i--) {
|
this.dep.depend()
|
||||||
this.deps[i].depend()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -216,4 +216,40 @@ describe('Options computed', () => {
|
|||||||
})
|
})
|
||||||
expect(() => vm.a).toThrowError('rethrow')
|
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)
|
}).then(done)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('lazy mode', done => {
|
it('computed mode, lazy', done => {
|
||||||
|
let getterCallCount = 0
|
||||||
const watcher = new Watcher(vm, function () {
|
const watcher = new Watcher(vm, function () {
|
||||||
|
getterCallCount++
|
||||||
return this.a + this.b.d
|
return this.a + this.b.d
|
||||||
}, null, { lazy: true })
|
}, null, { computed: true })
|
||||||
expect(watcher.lazy).toBe(true)
|
|
||||||
|
expect(getterCallCount).toBe(0)
|
||||||
|
expect(watcher.computed).toBe(true)
|
||||||
expect(watcher.value).toBeUndefined()
|
expect(watcher.value).toBeUndefined()
|
||||||
expect(watcher.dirty).toBe(true)
|
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.value).toBe(5)
|
||||||
expect(watcher.dirty).toBe(false)
|
expect(watcher.dirty).toBe(false)
|
||||||
|
|
||||||
|
// should not get again if not dirty
|
||||||
|
watcher.evaluate()
|
||||||
|
expect(getterCallCount).toBe(1)
|
||||||
|
|
||||||
vm.a = 2
|
vm.a = 2
|
||||||
waitForUpdate(() => {
|
waitForUpdate(() => {
|
||||||
|
expect(getterCallCount).toBe(1)
|
||||||
expect(watcher.value).toBe(5)
|
expect(watcher.value).toBe(5)
|
||||||
expect(watcher.dirty).toBe(true)
|
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.value).toBe(6)
|
||||||
expect(watcher.dirty).toBe(false)
|
expect(watcher.dirty).toBe(false)
|
||||||
}).then(done)
|
}).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 => {
|
it('teardown', done => {
|
||||||
const watcher = new Watcher(vm, 'b.c', spy)
|
const watcher = new Watcher(vm, 'b.c', spy)
|
||||||
watcher.teardown()
|
watcher.teardown()
|
||||||
|
Loading…
Reference in New Issue
Block a user