Caution using watchers for objects in Vue

Caution using watchers for objects in Vue

Suppose we have an object and we want to do something when it’s properties change. Probably we’ll start with something like that:

export default {
data: () {
return {
obj: {
prop: 1
}
}
},
watch: {
obj: {
deep: true, // <!-- This is important;
handler () {
...
}
}
}
}

deep modifier is important here since Vue does not compare nested object properties by default. And one may feel a temptation to use deep everywhere.

This short article admonishes you against doing that. Even more, I feel that my own code became more predictable and stable since I stopped using deep completely (and stopped watching objects at all).

Actually, the caveat about watching objects is that it’s hard to predict the behavior even for merely complex code. And it’s not about Vue — that’s just a way javascript works. I mean,

var a = {prop: 1}
var b = {prop: 1}
console.log(a === b) // <!-- FALSEvar c = {prop: 1}
var d = c
d.prop = 2
console.log(c === d, c.prop) // <!-- TRUE, 2

So, imagine we have some array of items in Vuex store. Suppose, one of them can be selected. And we don’t store selected property inside the item object itself, because it’s not optimal, and because we want to use normalizr approach. That’s how the store could look like:

const store = new Vuex.Store({
state: {
items: [{id: 1, name: 'First'}, {id: 2, name: 'Second'}],
selectedItemId: 1
},
mutations: {
selectItem (state, id) {
state.selectedItemId = id
}
}
})

But it’s convenient to have isSelected property, that’s why we add the following getter:

items (state) {
return state.items.map(item => ({
...item,
isSelected: item.id === state.selectedItemId
}))
}

Notice spread syntax here. We can not (and we should not) affect the item, stored in the state, that’s why we create new objects here. And items.map also creates a new array.

Ok, now imagine that the items can be reordered, and we want the order to be stored in cookies. We add the following watcher:

computed: {
items () { return this.$store.getters.items }
},
watch: {
items (items) {
const ids = items.map(item => item.id)
console.log('Storing ids..., ids)
}
}

Here you can play with the code: https://jsfiddle.net/kasheftin/nu5aezx1/15/

Notice that items watcher runs every time after changing selectedItemId. This happens because selectedItemId triggers items getter to rebuild, and the last returns the new array. It’s so simple and convenient to clone an object using array functions and ES6 syntax that we do it constantly, and that’s why the watcher can be triggered much more often then it should. Even if we do

computed: {
items () { return this.$store.getters.items },
itemIds () { return this.items.map(item => item.id) }
},
watch: {
itemIds (itemIds) {
console.log('Storing ids...', itemIds)
}
}

This is not the correct code as well. Intuition says that here we just extract ids from items, and the watcher should not trigger after selectedItemId change. But it does. items.map produces the new array every time.

Solution

Just use JSON.stringify:). If the object has circular dependencies, this one works just great: https://www.npmjs.com/package/circular-json-es6. Use it like this:

computed: {
items () { return this.$store.getters.items },
itemIdsTrigger () {
return JSON.stringify(this.items.map(item => item.id))
// For this simle case can be replaced with:
// return this.items.map(item => item.id).join(',)
}
},
watch: {
itemIdsTrigger () {
// We don't need itemIdsTrigger value itself;
// We don't extract it with JSON.parse;
// Just use initial this.items;
console.log('items order changed', this.items)
}
}
原文地址:https://www.cnblogs.com/chucklu/p/14242977.html