Consider a simple clock display and an input which I bound one-way to keep control over old/new state:
<div>{{ time }}</div>
<input ref="text" type="text" :value="text" style="width:95%">
<button type="button" @click="saveOnDiff">Save</button>
createApp({
...,
methods: {
saveOnDiff() {
const current = this.$refs.text.value;
// Compare current with old text update if it changed.
...
}
},
mounted() {
const instance = this;
setInterval(() => instance.time = new Date(), 1000);
}
}).mount('#app');
The clock is updated each second. Unfortunately, this update spoils the input. Try it here: https://jsfiddle.net/dL78tsh9
How can I reduce binding updates to the absolute necessary ones? Some extra switch on one-way bindings like :value.lazy="text"
would be helpful...
CodePudding user response:
Changing time
on each and every second will cause the whole template to be re-run after every 1 second. Which results everything in that template getting updated.
When a user types into <input>
element, You aren't storing that value anywhere. You've got a :value
to poke the value in but you aren't updating it when the value changes. The result will be that when Vue re-renders everything it will jump back to its original value.
Possible solution : Kindly ensure that your data is kept in sync with what the user types in. This could be done using v-model
and watcher to get new and old values and based on that you can achieve this requirmeent.
You can try something like this (This is not a perfect solution but it will give you an idea) :
const {
createApp
} = Vue
const characterWiseDiff = (left, right) => right
.split("")
.filter(function(character, index) {
return character != left.charAt(index);
})
.join("");
createApp({
data() {
return {
result: "",
text: "Try to change me here",
time: new Date(),
oldVal: null
}
},
watch: {
text(newVal, oldVal) {
this.oldVal = oldVal;
}
},
methods: {
saveOnDiff() {
if (!this.oldVal) this.oldVal = this.text
const current = this.$refs.text.value;
console.log(current, this.oldVal)
if (current === this.oldVal) {
this.result = "No changes have been made!";
} else {
this.result = `Saved! Your changes were: "${characterWiseDiff(current, this.oldVal)}"`;
}
}
},
mounted() {
const instance = this;
setInterval(() => instance.time = new Date(), 1000);
}
}).mount('#app');
<script src="https://unpkg.com/[email protected]/dist/vue.global.js"></script>
<div id="app">
<div>{{ time }}</div>
<input ref="text" type="text" v-model="text" style="width:95%">
<button type="button" @click="saveOnDiff">Save</button>
{{ result }}
</div>
CodePudding user response:
As far as I know, there's no way to trick VueJs to not re-render a specific field.
When the time changes, your existing virtual DOM has a value for "text" and the newly generated virtual DOM has a different value so... VueJS re renders it.
UPDATE:
Based on @Tolbxela comment, looks like you could use v-once to only render the field once, and ignore the future DOM updates.
https://vuejs.org/api/built-in-directives.html#v-once
Alternative
If you want to control old/new state, why don't you just use two-way binding and save both states?
Something like this:
const {
createApp
} = Vue
const characterWiseDiff = (left, right) => right
.split("")
.filter(function(character, index) {
return character != left.charAt(index);
})
.join("");
createApp({
data() {
return {
result: "",
text: "Try to change me here",
previousText: "Try to change me here",
time: new Date(),
}
},
methods: {
saveOnDiff() {
if (this.text === this.previousText) {
this.result = "No changes have been made!";
} else {
this.result = `Saved! Your changes were: "${characterWiseDiff(this.previousText, this.text)}"`;
this.previousText = this.text;
}
}
},
mounted() {
const instance = this;
setInterval(() => instance.time = new Date(), 1000);
}
}).mount('#app');
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<div>{{ time }}</div>
<div>
<input id="mockId" ref="text" type="text" v-model="text" style="width:95%">
<button type="button" @click="saveOnDiff">Save</button>
</div>
{{ result }}
</div>
https://jsfiddle.net/dmxLuf9w/6/
CodePudding user response:
The best solution is to not use the Vue reactivity for timer updates at all. See my UPDATE 2 below.
The simplest way to fix it is to replace :value
with v-model
UPDATE 1:
We need an other data field to store the input
value.
<input ref="text" type="text" v-model="input" style="width:95%">
Check the sample below.
But it is not a good solution for complex apps, since every second you whole app HTML is refreshed. This can cause problems with rendering and lags.
UPDATE 1:
I have missed the other logic of comparing values. Here is the well working solution
UPDATE 2:
This question helped me to understand the whole problem with template rendering in Vue.
TransitionGroup lag when using requestAnimationFrame
And here is a good article about Improve Vue Performance with v-once v-memo
CODE:
const placeholder = "Try to change me here"
const {
createApp
} = Vue
const characterWiseDiff = (left, right) => right
.split("")
.filter(function(character, index) {
return character != left.charAt(index);
})
.join("");
createApp({
data() {
return {
result: "",
text: placeholder,
input: placeholder,
time: new Date(),
}
},
methods: {
saveOnDiff() {
const current = this.input
if (current === this.text) {
this.result = "No changes have been made!";
} else {
this.result = `Saved! Your changes were: "${characterWiseDiff(this.text, current)}"`;
this.text = current;
}
}
},
mounted() {
const instance = this;
setInterval(() => instance.time = new Date(), 1000);
}
}).mount('#app');
<div id="app">
<div>{{ time }}</div>
<input ref="text" type="text" v-model="input" style="width:95%">
<button type="button" @click="saveOnDiff">Save</button>
{{ result }}
</div>
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>