diff --git a/src/store-util.js b/src/store-util.js index cea5d8b02..e3ba18609 100644 --- a/src/store-util.js +++ b/src/store-util.js @@ -1,4 +1,4 @@ -import { reactive, watch } from 'vue' +import { reactive, computed, watch, effectScope } from 'vue' import { forEachValue, isObject, isPromise, assert, partial } from './util' export function genericSubscribe (fn, subs, options) { @@ -29,6 +29,7 @@ export function resetStore (store, hot) { export function resetStoreState (store, state, hot) { const oldState = store._state + const oldScope = store._scope // bind store public getters store.getters = {} @@ -36,16 +37,23 @@ export function resetStoreState (store, state, hot) { store._makeLocalGettersCache = Object.create(null) const wrappedGetters = store._wrappedGetters const computedObj = {} - forEachValue(wrappedGetters, (fn, key) => { - // use computed to leverage its lazy-caching mechanism - // direct inline function use will lead to closure preserving oldState. - // using partial to return function with only arguments preserved in closure environment. - computedObj[key] = partial(fn, store) - Object.defineProperty(store.getters, key, { - // TODO: use `computed` when it's possible. at the moment we can't due to - // https://github.com/vuejs/vuex/pull/1883 - get: () => computedObj[key](), - enumerable: true // for local getters + const computedCache = {} + + // create a new effect scope and create computed object inside it to avoid + // getters (computed) getting destroyed on component unmount. + const scope = effectScope(true) + + scope.run(() => { + forEachValue(wrappedGetters, (fn, key) => { + // use computed to leverage its lazy-caching mechanism + // direct inline function use will lead to closure preserving oldState. + // using partial to return function with only arguments preserved in closure environment. + computedObj[key] = partial(fn, store) + computedCache[key] = computed(() => computedObj[key]()) + Object.defineProperty(store.getters, key, { + get: () => computedCache[key].value, + enumerable: true // for local getters + }) }) }) @@ -53,6 +61,10 @@ export function resetStoreState (store, state, hot) { data: state }) + // register the newly created effect scope to the store so that we can + // dispose the effects when this method runs again in the future. + store._scope = scope + // enable strict mode for new state if (store.strict) { enableStrictMode(store) @@ -67,6 +79,11 @@ export function resetStoreState (store, state, hot) { }) } } + + // dispose previously registered effect scope if there is one. + if (oldScope) { + oldScope.stop() + } } export function installModule (store, rootState, path, module, hot) { diff --git a/src/store.js b/src/store.js index 3ae6898f9..e22c74b5d 100644 --- a/src/store.js +++ b/src/store.js @@ -39,6 +39,12 @@ export class Store { this._modulesNamespaceMap = Object.create(null) this._subscribers = [] this._makeLocalGettersCache = Object.create(null) + + // EffectScope instance. when registering new getters, we wrap them inside + // EffectScope so that getters (computed) would not be destroyed on + // component unmount. + this._scope = null + this._devtools = devtools // bind commit and dispatch to self diff --git a/test/unit/modules.spec.js b/test/unit/modules.spec.js index e528f03fd..6805865f0 100644 --- a/test/unit/modules.spec.js +++ b/test/unit/modules.spec.js @@ -126,7 +126,7 @@ describe('Modules', () => { expect(mutationSpy).toHaveBeenCalled() }) - it.only('should keep getters when component gets destroyed', async () => { + it('should keep getters when component gets destroyed', async () => { const store = new Vuex.Store() const moduleA = { @@ -165,7 +165,7 @@ describe('Modules', () => { await nextTick() store.commit('moduleA/increment') - console.log(store.state.moduleA.value) + expect(store.getters['moduleA/getState']).toBe(2) }) })