diff --git a/src/core/instance/state.js b/src/core/instance/state.js index 326f7f36bf7..b1549b0dcc4 100644 --- a/src/core/instance/state.js +++ b/src/core/instance/state.js @@ -2,7 +2,7 @@ import config from '../config' import Watcher from '../observer/watcher' -import { pushTarget, popTarget } from '../observer/dep' +import Dep, { pushTarget, popTarget } from '../observer/dep' import { isUpdatingChildComponent } from './lifecycle' import { @@ -164,7 +164,7 @@ export function getData (data: Function, vm: Component): any { } } -const computedWatcherOptions = { computed: true } +const computedWatcherOptions = { lazy: true } function initComputed (vm: Component, computed: Object) { // $flow-disable-line @@ -244,8 +244,13 @@ function createComputedGetter (key) { return function computedGetter () { const watcher = this._computedWatchers && this._computedWatchers[key] if (watcher) { - watcher.depend() - return watcher.evaluate() + if (watcher.dirty) { + watcher.evaluate() + } + if (Dep.target) { + watcher.depend() + } + return watcher.value } } } diff --git a/src/core/observer/watcher.js b/src/core/observer/watcher.js index 725480d9215..3c4e533c08b 100644 --- a/src/core/observer/watcher.js +++ b/src/core/observer/watcher.js @@ -29,11 +29,10 @@ export default class Watcher { id: number; deep: boolean; user: boolean; - computed: boolean; + lazy: boolean; sync: boolean; dirty: boolean; active: boolean; - dep: Dep; deps: Array; newDeps: Array; depIds: SimpleSet; @@ -58,16 +57,16 @@ export default class Watcher { if (options) { this.deep = !!options.deep this.user = !!options.user - this.computed = !!options.computed + this.lazy = !!options.lazy this.sync = !!options.sync this.before = options.before } else { - this.deep = this.user = this.computed = this.sync = false + this.deep = this.user = this.lazy = this.sync = false } this.cb = cb this.id = ++uid // uid for batching this.active = true - this.dirty = this.computed // for computed watchers + this.dirty = this.lazy // for lazy watchers this.deps = [] this.newDeps = [] this.depIds = new Set() @@ -90,12 +89,9 @@ export default class Watcher { ) } } - if (this.computed) { - this.value = undefined - this.dep = new Dep() - } else { - this.value = this.get() - } + this.value = this.lazy + ? undefined + : this.get() } /** @@ -166,24 +162,8 @@ export default class Watcher { */ update () { /* istanbul ignore else */ - 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() - }) - } + if (this.lazy) { + this.dirty = true } else if (this.sync) { this.run() } else { @@ -197,54 +177,47 @@ export default class Watcher { */ run () { if (this.active) { - this.getAndInvoke(this.cb) - } - } - - getAndInvoke (cb: Function) { - const value = this.get() - if ( - value !== this.value || - // Deep watchers and watchers on Object/Arrays should fire even - // when the value is the same, because the value may - // have mutated. - isObject(value) || - this.deep - ) { - // set new value - const oldValue = this.value - this.value = value - this.dirty = false - if (this.user) { - try { - cb.call(this.vm, value, oldValue) - } catch (e) { - handleError(e, this.vm, `callback for watcher "${this.expression}"`) + const value = this.get() + if ( + value !== this.value || + // Deep watchers and watchers on Object/Arrays should fire even + // when the value is the same, because the value may + // have mutated. + isObject(value) || + this.deep + ) { + // set new value + const oldValue = this.value + this.value = value + if (this.user) { + try { + this.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) } - } else { - cb.call(this.vm, value, oldValue) } } } /** - * Evaluate and return the value of the watcher. - * This only gets called for computed property watchers. + * Evaluate the value of the watcher. + * This only gets called for lazy watchers. */ evaluate () { - if (this.dirty) { - this.value = this.get() - this.dirty = false - } - return this.value + this.value = this.get() + this.dirty = false } /** - * Depend on this watcher. Only for computed property watchers. + * Depend on all deps collected by this watcher. */ depend () { - if (this.dep && Dep.target) { - this.dep.depend() + let i = this.deps.length + while (i--) { + this.deps[i].depend() } } diff --git a/test/unit/features/options/computed.spec.js b/test/unit/features/options/computed.spec.js index fc606072e7d..edc20bad3bf 100644 --- a/test/unit/features/options/computed.spec.js +++ b/test/unit/features/options/computed.spec.js @@ -216,40 +216,4 @@ 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: `
{{ a }}
`, - 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) - }) }) diff --git a/test/unit/modules/observer/watcher.spec.js b/test/unit/modules/observer/watcher.spec.js index ba96b28ffce..724a3cc8637 100644 --- a/test/unit/modules/observer/watcher.spec.js +++ b/test/unit/modules/observer/watcher.spec.js @@ -144,70 +144,26 @@ describe('Watcher', () => { }).then(done) }) - it('computed mode, lazy', done => { - let getterCallCount = 0 + it('lazy mode', done => { const watcher = new Watcher(vm, function () { - getterCallCount++ return this.a + this.b.d - }, null, { computed: true }) - - expect(getterCallCount).toBe(0) - expect(watcher.computed).toBe(true) + }, null, { lazy: true }) + expect(watcher.lazy).toBe(true) expect(watcher.value).toBeUndefined() expect(watcher.dirty).toBe(true) - expect(watcher.dep).toBeTruthy() - - const value = watcher.evaluate() - expect(getterCallCount).toBe(1) - expect(value).toBe(5) + watcher.evaluate() 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) - - const value = watcher.evaluate() - expect(getterCallCount).toBe(2) - expect(value).toBe(6) + watcher.evaluate() 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()