Skip to content

Commit b2f851f

Browse files
authored
fix: getters being destroyed on component destroy (#1878) (#1883)
fix #1878
1 parent 3c56e9a commit b2f851f

File tree

6 files changed

+178
-98
lines changed

6 files changed

+178
-98
lines changed

package.json

+4-4
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@
5252
},
5353
"homepage": "https://github.com/vuejs/vuex#readme",
5454
"peerDependencies": {
55-
"vue": "^3.0.2"
55+
"vue": "^3.2.0"
5656
},
5757
"dependencies": {
5858
"@vue/devtools-api": "^6.0.0-beta.11"
@@ -65,7 +65,7 @@
6565
"@rollup/plugin-node-resolve": "^13.0.0",
6666
"@rollup/plugin-replace": "^2.4.2",
6767
"@types/node": "^15.6.0",
68-
"@vue/compiler-sfc": "^3.0.11",
68+
"@vue/compiler-sfc": "^3.2.4",
6969
"babel-jest": "^26.6.3",
7070
"babel-loader": "^8.2.2",
7171
"brotli": "^1.3.2",
@@ -89,8 +89,8 @@
8989
"todomvc-app-css": "^2.4.1",
9090
"typescript": "^4.2.4",
9191
"vitepress": "^0.11.5",
92-
"vue": "^3.0.11",
93-
"vue-loader": "^16.2.0",
92+
"vue": "^3.2.4",
93+
"vue-loader": "^16.5.0",
9494
"vue-style-loader": "^4.1.3",
9595
"webpack": "^4.43.0",
9696
"webpack-dev-middleware": "^3.7.2",

src/store-util.js

+28-11
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { reactive, watch } from 'vue'
1+
import { reactive, computed, watch, effectScope } from 'vue'
22
import { forEachValue, isObject, isPromise, assert, partial } from './util'
33

44
export function genericSubscribe (fn, subs, options) {
@@ -29,30 +29,42 @@ export function resetStore (store, hot) {
2929

3030
export function resetStoreState (store, state, hot) {
3131
const oldState = store._state
32+
const oldScope = store._scope
3233

3334
// bind store public getters
3435
store.getters = {}
3536
// reset local getters cache
3637
store._makeLocalGettersCache = Object.create(null)
3738
const wrappedGetters = store._wrappedGetters
3839
const computedObj = {}
39-
forEachValue(wrappedGetters, (fn, key) => {
40-
// use computed to leverage its lazy-caching mechanism
41-
// direct inline function use will lead to closure preserving oldState.
42-
// using partial to return function with only arguments preserved in closure environment.
43-
computedObj[key] = partial(fn, store)
44-
Object.defineProperty(store.getters, key, {
45-
// TODO: use `computed` when it's possible. at the moment we can't due to
46-
// https://github.com/vuejs/vuex/pull/1883
47-
get: () => computedObj[key](),
48-
enumerable: true // for local getters
40+
const computedCache = {}
41+
42+
// create a new effect scope and create computed object inside it to avoid
43+
// getters (computed) getting destroyed on component unmount.
44+
const scope = effectScope(true)
45+
46+
scope.run(() => {
47+
forEachValue(wrappedGetters, (fn, key) => {
48+
// use computed to leverage its lazy-caching mechanism
49+
// direct inline function use will lead to closure preserving oldState.
50+
// using partial to return function with only arguments preserved in closure environment.
51+
computedObj[key] = partial(fn, store)
52+
computedCache[key] = computed(() => computedObj[key]())
53+
Object.defineProperty(store.getters, key, {
54+
get: () => computedCache[key].value,
55+
enumerable: true // for local getters
56+
})
4957
})
5058
})
5159

5260
store._state = reactive({
5361
data: state
5462
})
5563

64+
// register the newly created effect scope to the store so that we can
65+
// dispose the effects when this method runs again in the future.
66+
store._scope = scope
67+
5668
// enable strict mode for new state
5769
if (store.strict) {
5870
enableStrictMode(store)
@@ -67,6 +79,11 @@ export function resetStoreState (store, state, hot) {
6779
})
6880
}
6981
}
82+
83+
// dispose previously registered effect scope if there is one.
84+
if (oldScope) {
85+
oldScope.stop()
86+
}
7087
}
7188

7289
export function installModule (store, rootState, path, module, hot) {

src/store.js

+6
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,12 @@ export class Store {
3939
this._modulesNamespaceMap = Object.create(null)
4040
this._subscribers = []
4141
this._makeLocalGettersCache = Object.create(null)
42+
43+
// EffectScope instance. when registering new getters, we wrap them inside
44+
// EffectScope so that getters (computed) would not be destroyed on
45+
// component unmount.
46+
this._scope = null
47+
4248
this._devtools = devtools
4349

4450
// bind commit and dispatch to self

test/helpers.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import puppeteer from 'puppeteer'
44
export function mount (store, component) {
55
const el = createElement()
66

7-
component.render = () => {}
7+
component.render = component.render || (() => {})
88

99
const app = createApp(component)
1010

test/unit/modules.spec.js

+52-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { nextTick } from 'vue'
1+
import { h, nextTick } from 'vue'
2+
import { mount } from 'test/helpers'
23
import Vuex from '@/index'
34

45
const TEST = 'TEST'
@@ -124,6 +125,56 @@ describe('Modules', () => {
124125
store.commit('a/foo')
125126
expect(mutationSpy).toHaveBeenCalled()
126127
})
128+
129+
it('should keep getters when component gets destroyed', async () => {
130+
const store = new Vuex.Store()
131+
132+
const spy = jest.fn()
133+
134+
const moduleA = {
135+
namespaced: true,
136+
state: () => ({ value: 1 }),
137+
getters: {
138+
getState (state) {
139+
spy()
140+
return state.value
141+
}
142+
},
143+
mutations: {
144+
increment: (state) => { state.value++ }
145+
}
146+
}
147+
148+
const CompA = {
149+
template: `<div />`,
150+
created () {
151+
this.$store.registerModule('moduleA', moduleA)
152+
}
153+
}
154+
155+
const CompB = {
156+
template: `<div />`
157+
}
158+
159+
const vm = mount(store, {
160+
components: { CompA, CompB },
161+
data: () => ({ show: 'a' }),
162+
render () {
163+
return this.show === 'a' ? h(CompA) : h(CompB)
164+
}
165+
})
166+
167+
expect(store.getters['moduleA/getState']).toBe(1)
168+
expect(spy).toHaveBeenCalledTimes(1)
169+
170+
vm.show = 'b'
171+
await nextTick()
172+
173+
store.commit('moduleA/increment')
174+
175+
expect(store.getters['moduleA/getState']).toBe(2)
176+
expect(spy).toHaveBeenCalledTimes(2)
177+
})
127178
})
128179

129180
// #524

0 commit comments

Comments
 (0)