watchEffect() was triggered when the 'property' of array has real changed
What problem does this feature solve?
I put the 'property' in quote, it is a analogy. Specifically, indexOf(T): number
method is used here as an example.
As we know, Vue helps us filter out 'fake' change events when observing a common property.
const count = ref(0)
watchEffect(() => console.log(count.value))
const click => count.value = 1
Initializing the above code will immediately print a 0
. When the first click , a 1
will be printed, but continued clicking will no longer be executed. It was as expected and it worked well.
However, this behavior will not be respected in arrays.
const arr = reactive([])
watchEffect(() => console.log(arr.indexOf('123456')))
const click => arr.push('654321')
When I keep clicking the button, it will keep printing -1
. But in fact, the return value of .indexOf('123456')
does not change, just as the return value of count.value
does not change.
What does the proposed API look like?
I expect the same behavior for 'read methods' in arrays as for getter properties to perform side effects when real changes occur.
It will change in the example.
@liulinboyi
Maybe my English is pool, I mean the -1
is being printed repeatedly, which is NOT expected behavior.
The change caused by push('654321')
does not cause the return value of indexOf('123456')
to be changed, so I think the effect should NOT be executed.
It will change in the example.
@liulinboyi Maybe my English is pool, I mean the
-1
is being printed repeatedly, which is NOT expected behavior. The change caused bypush('654321')
does not cause the return value ofindexOf('123456')
to be changed, so I think the effect should NOT be executed.
I didn't understand correctly😉. About this issue, in fact Vue can't predict the result.
About this issue, in fact Vue can't predict the result.
Yes, to be honest, I found this issue because I was writing a similar reactivity component for WPF (C#) and when I started to implement support for list, I ran into this issue. Unlike properties, read members and write members in collections are completely different and cannot capture them within the same interceptor.
Here is my initial solution:
- Intercept read methods, cache method names and return values (or cache parameters, depending on which is the value type, to avoid memory leaks).
- Intercept write methods and determine which read methods may be triggered.
- Check if the effect to be triggered contains these read methods, and if it does, determine if it was REALLY changed by the cached values.
I think my solution is very cumbersome and likely to have unforeseen vulnerabilities, so I referenced Vue's source code, but found that Vue do nothing about it.
You can process in you source code, like example
<script setup>
import { ref, reactive, watchEffect, computed } from 'vue'
const arr = reactive([])
const res = reactive([])
let oldVal = null
watchEffect(() => {
debugger
let newVal = arr.lastIndexOf('123456')
if (!Object.is(oldVal,newVal)) {
// update oldVal
oldVal = newVal
// do something
// eg
console.log(newVal)
res.push(newVal)
}
})
const click = () => arr.push((Math.floor(Math.random() * 100)) % 2 ? '654321' : '123456')
</script>
<template>
<div>
{{ arr }}
</div>
<button @click="click">
Change
</button>
<div>
{{ res }}
</div>
</template>
This is expected behavior - watchEffect
always runs whenever any of its dependencies change.
If you want to avoid running an effect when a certain condition doesn't change, use watch
:
watch(() => arr.indexOf('123456'), index => {
// only runs if index changes
})
This is expected behavior -
watchEffect
always runs whenever any of its dependencies change.If you want to avoid running an effect when a certain condition doesn't change, use
watch
:watch(() => arr.indexOf('123456'), index => { // only runs if index changes })
@yyx990803 Thanks for the reply.
Is this a deliberately designed behavior, or a compromise due to the difficulty of implementation?
I tend to favor the latter because the behavior of array is inconsistent with normal properties. If this is by design, I can't understand what kind of consideration was given to it. Can you provide more information? Thank you very much. As an example, if I want to write logs with watchEffect()
, then this will result in writing the same messages.
- For common properties, in the set interceptor, only the same property name is required to obtain old-value and new-value, it is easy to determine if a change has really occurred.
- But for arrays, names of read and write method are completely inconsistent and come with parameters.
For watch
, the old-value can be cached explicitly since it only has a unique return value. However, this workaround will be invalid if there are multiple read methods of the array in a effect.
Your code literally mean "run console.log(arr.indexOf('123456'))
" when arr
is changed, while your intended behavior is "run console.log(arr.indexOf('123456'))
" when arr.indexOf('123456')
is changed. I don't think we can/should infer this from watchEffect
.
Your code literally mean "
run console.log(arr.indexOf('123456'))
" when arr is changed
@Justineo Thanks, I understand the design idea of it.
I don't think we can/should infer this from
watchEffect
.
For "should", the reason I have this doubt from the behavior of watching properties, I think properties are methods (ignoring syntactic sugar), which are all members of an object.
For "can", if in C# (in fact, I'm not familiar with JavaScript), I think we can cache function () => Reflect.invoke(target, 'indexOf', [args])
(pseudo-code) to Dep while tracing indexOf(args)
, and then execute it at trigger push()
.
pseudo-code (I'm not a qualified js user and I'm not sure it needs to handle more cases, dynamic languages are more complex)
// in proxy:
function interceptor(ctx) {
if (ctx.key === 'indexOf' /* or other read method */) {
track(ctx.target, ctx.key, getter: () => Reflect.invoke(ctx.target, 'indexOf', ctx.args))
} else if (ctx.key === 'push' /* or other write method */) {
const getter = getGetterRelateToThisOperation(ctx.target, ctx.key)
const oldValue = getter()
ctx.next() // in JavaScript's Proxy API, not like a middleware pattern? non-CPS.
const newValue = getter()
if (oldValue !== newValue) {
trigger(target, ctx.key)
}
}
}