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)
}
}

 

作者:Chuck Lu    GitHub    
posted @   ChuckLu  阅读(57)  评论(0编辑  收藏  举报
编辑推荐:
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
历史上的今天:
2016-01-06 律师的最高境界:呆若木鸡
2015-01-06 svn根据项目来创建目录结构或者根据分支来创建项目结构
点击右上角即可分享
微信分享提示