Hi,
If I'm not mistaken, currently the getter cache only works when it does not return a function:
simpleGetter: state => `${state.value1}-${state.value2}`, // only execute the first time or when value1/value2 are updated
advancedGetter: state => arg => `${arg}-${state.value1}` // execute everytime
I thought (the documentation does not specify it) that the getter "advancedGetter" was cached according to the argument (and 2 calls to the function with the same argument runs only once).
Would not it be possible to have a similar cache system? By looking at what properties are used (state and other getters), and when one of them is invalidated, re-run the getter?
I think this is almost impossible because the inner function of getters are absolutely separated by Vuex store. That means, the inner function cannot aware the state update and can return incorrect value.
Example:
const store = new Vuex.Store({
state: {
value: 1
},
getters: {
// Assume the result of inner function will be cached...
foo: state => n => {
return state.value + n
}
}
})
const f = store.getters.foo
f(1) // -> 2
store.state.value = 10
f(1) // -> 2 (returns incorrect cached value because `f` cannot aware state update)
Yeah but as the "simple getters", it is possible to look the properties used (state and getters), and watch them? If one of these properties is updated, the watch is triggered, and the cache should then be invalidated. The simple getters work like that, no?
How would you detect which properties are used in the inner function? AFAIK, we cannot do that in Vuex layer.
Anyway if we can do this, the implementation seems too complex and the runtime overhead is not so small. I doubt it's worth adding.
I don't really know how Vue.js works behind, but I consider that is an important feature (and not just because i need it). Personally I even thought it was already the case, until I came to debug my application.
Concerning the feasibility of the functionality, I did not manage to realize it, because of my weak knowledge about the framework. I am close to a functional solution which in my opinion is not runtime resources expensive (it allows a cache system and applies only to functions). Maybe you or @yyx990803 could improve or give me a feedback on this POC:
// src/store.js
// function resetStoreVM
forEachValue(wrappedGetters, (fn, key) => {
// use computed to leverage its lazy-caching mechanism
computed[key] = () => fn(store)
Object.defineProperty(store.getters, key, {
get: () => {
const value = store._vm[key]
if (typeof value === 'function') {
return function () {
const args = [].slice.call(arguments)
const fullKey = key + '#' + JSON.stringify(args)
if (store._vm[fullKey] === undefined) {
store._vm.$watch(() => value.apply(this, args), result => {
store._vm[fullKey] = result
}, { immediate: true })
}
return store._vm[fullKey]
}
}
return value
},
enumerable: true // for local getters
})
})
// test/unit/store.spec.js
it('getters cache with arguments', () => {
let counterTest = 0
let counterTest2 = 0
const store = new Vuex.Store({
state: {
a: 1,
b: 1,
c: 1
},
getters: {
test: state => {
counterTest++
return state.a + 1
},
test2: (state, getters) => num => {
counterTest2++
return state.b + getters.test + num
}
},
mutations: {
[TEST] (state, key) {
state[key] += 1
}
}
})
store.commit(TEST, 'a')
expect(store.getters.test2(1)).toBe(5)
expect(store.getters.test2(1)).toBe(5)
expect(counterTest).toBe(1)
expect(counterTest2).toBe(1)
store.commit(TEST, 'a')
expect(store.getters.test2(1)).toBe(6)
expect(store.getters.test2(1)).toBe(6)
expect(counterTest).toBe(2)
expect(counterTest2).toBe(2)
})
If you do not want to follow up on this feature (which I can understand), what would you suggest to work around this problem on the application side?
Thank you for your time,
Aur茅lien
Oh, I didn't notice we can use the inner function as watcher handler. It's clever 馃憤
My concern is that this is a push-based reactive system - functions in all getters are immediately evaluated when the state is updated in this implementation. I'm not sure how this impacts the performance.
Probably @yyx990803 would give more useful feedback.
I just updated the source code: the problem was I did not use the nextTick function in my tests. Here is an improved version:
// src/store.js
// function resetStoreVM
forEachValue(wrappedGetters, (fn, key) => {
// use computed to leverage its lazy-caching mechanism
computed[key] = () => fn(store)
Object.defineProperty(store.getters, key, {
get: () => {
const value = store._vm[key]
if (typeof value === 'function') {
return function () {
const args = [].slice.call(arguments)
const fullKey = key + '#' + JSON.stringify(args)
if (store._vm[fullKey] === undefined) {
store._vm.$watch(() => value.apply(this, args), result => {
store._vm[fullKey] = result
}, { immediate: true })
}
return store._vm[fullKey]
}
}
return value
},
enumerable: true // for local getters
})
})
// test/unit/store.spec.js
it('getters cache with arguments', done => {
let counterTest = 0
let counterTest2 = 0
const store = new Vuex.Store({
state: {
a: 1,
b: 1,
c: 1
},
getters: {
test: state => {
counterTest++
return state.a + 1
},
test2: (state, getters) => num => {
counterTest2++
return state.b + getters.test + num
}
},
mutations: {
[TEST] (state, key) {
state[key] += 1
}
}
})
store.commit(TEST, 'a')
expect(store.getters.test2(1)).toBe(5)
expect(store.getters.test2(1)).toBe(5)
expect(counterTest).toBe(1)
expect(counterTest2).toBe(1) // vs 2
store.commit(TEST, 'a')
Vue.nextTick(() => {
expect(store.getters.test2(1)).toBe(6)
expect(store.getters.test2(1)).toBe(6)
expect(counterTest).toBe(2)
expect(counterTest2).toBe(2) // vs 4
expect(store.getters.test2(2)).toBe(7)
expect(store.getters.test2(3)).toBe(8)
expect(store.getters.test2(2)).toBe(7)
expect(store.getters.test2(1)).toBe(6)
expect(store.getters.test2(3)).toBe(8)
expect(counterTest).toBe(2)
expect(counterTest2).toBe(4) // vs 9
store.commit(TEST, 'b') // re-run test2(1)+test2(2)+test2(3)
Vue.nextTick(() => {
expect(counterTest).toBe(2)
expect(counterTest2).toBe(7) // vs 9
expect(store.getters.test2(1)).toBe(7)
expect(store.getters.test2(2)).toBe(8)
expect(store.getters.test2(3)).toBe(9)
expect(store.getters.test2(4)).toBe(10)
expect(counterTest).toBe(2)
expect(counterTest2).toBe(8) // vs 13 - only the test2(4)
store.commit(TEST, 'c') // nothing
Vue.nextTick(() => {
expect(counterTest).toBe(2)
expect(counterTest2).toBe(8) // vs 13
done()
})
})
})
})
I see two possible issues for this implementation:
$watch method in order to remove the cache instead of re-evaluate it? By the way, is there a difference between store._watcherVM.$watch and store._vm.$watch, or we do not care?JSON.stringify call, to have a cache-key for the current arguments. Not sure that is safe/reliable, in case of advanced argument (e.g. objects or functions). And this is surely not the less expensive function.What about having another way to declare the getters with arguments when we want to have a cache system, something like that:
const getters = {
getUser: (state, getters) => ({
cacheKey: (team, slug) => `${team}#${slug}`,
get: (team, slug) => {
if (state.users[team] && state.users[team][slug]) {
return state.users[team][slug]
}
return null
}
})
}
Then we could have a cache system for the getters with arguments only for the one which has a cacheKey property. What do you think about that?
Hmm, I don't think we can introduce special return value in getters since it returns any values.
BTW, I just realized we can implement it out of Vuex. The experimental implementation is here.
I now personally think it would be better not to include it in Vuex because the returning function is just a pattern but not a part of Vuex API.
Ok, I understand. Anyway, thank you for your responses and your help.
I just had a look on your implementation: this is indeed clean and smart, and even if I think this kind of functionality should be directly integrated to Vuex, I understand your desire to not weigh the library. I thought in all cases doing something similar, but I do not think I will have been able to do it using the Vue engine. I really appreciate that you did it.
However, is it wrong if I say that your library seems to only keep one cache by getter? I do not yet test it, but by reading the source code, I have the impression that this example will not use the cache (sorry if I am wrong):
getters.getTodoById(1)
getters.getTodoById(2)
getters.getTodoById(1)
getters.getTodoById(2)
Yes, it caches only previous call as same as Vue's computed properties. I think it can be easily extended by using a hash of cache object to cache any calls. But it would also have more overheads.
Ok, that is what I thought. In any case this will make me a very good base for my implementation on the application side, I will begin with your creation. For my needs, I think I just need to add the possibility of keeping several caches in parallel for a same getter.
If you do not think this has its place in Vuex directly, I let you close the issue. Thank you again for your responsiveness and your help!
OK, as I said, I think this would be better to be achieved out of Vuex.
Thanks for your suggestion, anyway 馃檪
I would love vuex to cache results with argumented getters.
I have a recursive tree with items, each having a certain amount of used_minutes set.
I need every single node to calculate the total amount of used_minutes of itself and all it's children, grandchildren, etc recursively.
When I do this with one getter function to which I feed the item ID as argument, the vue caching does not work, even if I refer it in the vue component's computed property as well.
vuex-strong-cache also didn't work for me.
Hi @mesqueeb,
Here is the code that I'm using _to fix this issue_:
import Vue from 'vue'
export function strongCache (config) {
if (typeof config === 'function') {
config = { getter: config }
}
const vms = {}
return (state, getters, rootState, rootGetters) => {
const fn = config.getter(state, getters, rootState, rootGetters)
if (typeof fn !== 'function') {
return fn
}
return (...args) => {
const key = 'c:' + (config.cacheKey ? config.cacheKey(...args) : args.join('~'))
if (!vms[key]) {
vms[key] = new Vue({
computed: {
value () {
return fn(...args)
}
}
})
}
return vms[key].value
}
}
}
export function strongCaches (getters) {
Object.keys(getters).forEach(getter => {
getters[getter] = strongCache(getters[getter])
})
return getters
}
To use it, call the strongCache function on each of your getters where you want some strong cache, or call strongCaches function on the getters object of a module.
Important note: I strongly recommend understanding the code before using it, and making sure that it has the right behavior for your project. As you can see, this works well with native types (numbers or strings without ~), but not with objects. If you want to cache getters with advanced arguments, use a cacheKey function, or edit the source code to use something like JSON.stringify (be careful not to significantly reduce performance).
@Dewep after some study I understand now that you would create a new Vue instance for every item I would want to have cached getters and set them as computed properties.
I just wonder one thing: doesn't this reduce performance when you have over 1000 items with several getters you want cached per item?
I thought of something similar, could you have a look and let me know what you think?
window.getItemVue = function(itemId) {
if (cachedItemVues[itemId]) { return cachedItemVues[itemId]}
console.log(`creating new Vue for item ${itemId}!`)
return cachedItemVues[itemId] = new Vue({
data: {
id: itemId,
item: store.state.items[itemId],
},
computed: {
dist() {
console.log(`calculating dist of item ${this.id}!`)
return Math.sqrt(this.item.x * this.item.x + this.item.y * this.item.y)
},
// all getters I want cached per recursive item can be rewritten as a computed property here.
},
})
}
Here it is in action in Codepen: https://codepen.io/mesqueeb/pen/KqLGvX?editors=1111
The reason that I strongly recommended to understand the code before using it, is because I'm not able to answer to your question. As far as I know, it is better for my application, but I'm not sure to the result with a lot of getters, or with a lot of different arguments for a same getter.
Two weeks before I created this issue, I had never written a line of Vue.js. Your code is kindly similar to mine (except that you are using data in addition to computed, but I don't think it changes much), so I will say that the performances are identical: it is much better for fifty getters, with at most a hundred different possible arguments for each. For a bigger project, I don't think I'm able to advise you, I'm clearly not an expert of Vue.js.
After my todo-app has grown in size, and several users have 1000+ items, I'm running in very heavy performance issues.
Maybe it's because of Vue devtools trying to attach on each Vue instance? (1000+ instances), but I'm seeing Chrome use by average 1.2 GB of memory.
One vue instance per item is a real performance dumper I think, but I have search the web far and wide, and I cannot think of any other method to have cached computed properties per item.
If anyone knows any advice for me: a manual JS class with cached getters? (is this possible?) Or another way to have cached computed properties per item when the DB is about 1000 items long, please let me know!
Although my issue is one year old, it is still a real issue for me. I still use the code I provided above, and although I haven't had any problems with it since, I don't have as many items to cache as you do.
With the performance that this can add, I find it interesting to talk about it again, and I sincerely think that Vue developers should look more into the subject.
More than that, I'm convinced that many developers think Vue manages a cache for these getters functions. :)
@Dewep YES you are right, the only mention otherwise is a tiny passive mention of this issue at the end of this section of documentation:
https://vuex.vuejs.org/getters.html#property-style-access
IMO it should be highlighted as a very important thing to understand before considering getters with arguments.
For using keyed caching, I suggest something like an md5 hash of the arguments instead of a stringified list of the arugments, since the former will use less memory and match lookup is less overhead.
@Dewep I agree, ran into this myself and was surprised Vue doesn't offer this functionality. Having the ability to optionally cache function based getters seems like a great idea.
Most helpful comment
Although my issue is one year old, it is still a real issue for me. I still use the code I provided above, and although I haven't had any problems with it since, I don't have as many items to cache as you do.
With the performance that this can add, I find it interesting to talk about it again, and I sincerely think that Vue developers should look more into the subject.
More than that, I'm convinced that many developers think Vue manages a cache for these getters functions. :)