前言
使用watch监听为什么有时不生效? 这篇文章或许可以给你答案,看完还不懂,请来找我。
1.列表渲染中修改数组元素(对象)的某个属性,但不能触发视图更新。
举个栗子
父组件有个element对象, 每次添加商品时需要往element.data里面去添加一条数据, 以此来更新父组件以及子组件的内容,通过$emit的形式更新父组件的element。
代码如下:
<div class="item-box" v-for="(product, paramIndex) in element.data" :key='paramIndex'>
<div class="wj-item wj-border">
<v-image :src="product.thumbUrl"></v-image>
<div class="wj-name">{{product.skuName}}</div>
<div class="wj-price">{{product.retailPrice | currency}}</div>
</div>
<div class="wj-right-bar lCenter right">
<div class="wj-arrow-icon wj-top-red" v-show="paramIndex != 0" @click="removeTop(paramIndex)"></div>
<div class="wj-arrow-icon wj-bottom-red" v-show="paramIndex != element.data.length - 1" @click="removeBottom(paramIndex)"></div>
<div class="wj-arrow-icon wj-delete" @click="deleteOptions(paramIndex)"></div>
</div>
</div>
props: {
element: {
type: Object,
required: true
}
},
methods:{
addProduct() {
this.$emit('show.search', 'multiple', this.element.data)
},
}
然而element.name等其他属性可以显示, 但element.data里面的数据却没有更新。
原因是 由于 JavaScript 的限制,Vue 不能检测数组和对象的变化。
对于无法监听数组的改动,官方提供了两种方式:
// (1).Vue.set
Vue.set(vm.items, indexOfItem, newValue)
//(2). Array.prototype.splice
vm.items.splice(indexOfItem, 1, newValue)
但对于上述情况,一个个去赋值,并不是一个好的方式,那么可以使用watch进行监听。
watch: {
'element.data': function(val) {
console.log(this.element.data);//可以拿到数据
},
},
这样就可以监听到element.data数组里面的改动了。
tips: (1) watch监听优化: 监听某个对象时,对象的任何属性改变都会触发变动, 这样比较耗性能, 如果明确知道只需监听某一属性,可以使用字符串的形式监听,如’element.data’。
(2) watch有一个特点,就是当值第一次绑定的时候,不会执行监听函数,只有值发生改变才会执行。如果我们需要在最初绑定值的时候也执行函数,可添加immediate属性。
(3) 普通watch方法不能监听对象内部属性的变化,可以添加deep属性深度监听。
那么watch内部是如何实现监听的呢?一起来看看watch的内部实现。
watch的内部原理解析
要知道watch的工作原理, 需要了解三个地方:
a.监听数据改变时,watch 如何工作
b.设置 immediate 时,watch 如何工作
c.设置了 deep 时,watch 如何工作
a.监听数据改变时, watch是如何工作的?
Vue会把数据设置响应式,即设置他的 get 和 set
当数据被读取,get被触发,然后收集到读取他的东西,保存到依赖收集器
当数据被改变,set被触发,然后通知曾经读取他的东西进行更新。
watch 在一开始初始化的时候,会 读取 一遍 监听的数据的值,于是,此时 那个数据就收集到 watch 的 watcher 了
然后 你给 watch 设置的 handler ,watch 会放入 watcher 的更新函数中
当 数据改变时,通知 watch 的 watcher 进行更新,于是 你设置的 handler 就被调用了.
a.设置了immediate , watch是如何工作的?
设置了 immediate 时,就不需要在数据改变的时候才会触发。
而是在 初始化 watch 时,在读取了 监听的数据的值 之后,便立即调用一遍你设置的监听回调,然后传入刚读取的值.
a.设置了deep , watch是如何工作的?
watch 有一个 deep 选项,是用来深度监听的,什么是深度监听呢?就是当你监听的属性的值是一个对象的时候,如果你没有设置深度监听,当对象内部变化时,你监听的回调是不会被触发的.
function _traverse (val: any, seen: SimpleSet) {
let i, keys
const isA = Array.isArray(val) //是否为数组
if ((!isA && !isObject(val)) // 如果不是array和object
|| Object.isFrozen(val) // 或者是已经冻结对象
|| val instanceof VNode) // 或者是VNode实例
{
return
}
if (val.__ob__) {//只有object和array才有__ob__属性
const depId = val.__ob__.dep.id
if (seen.has(depId)) { // 已经有收集过
return
}
seen.add(depId) //没有被收集过
}
if (isA) { //如果是数组, 递归进行收集
i = val.length
while (i--) _traverse(val[i], seen)
} else {
keys = Object.keys(val)
i = keys.length
while (i--) _traverse(val[keys[i]], seen)
}
}
从vue源码可以看到, 当存在deep属性时,会执行traverse方法。
简单来讲,就是递归收集对象或数组的子属性值。
tips: 需要注意的是,不应该使用箭头函数来定义 watcher 函数 (例如 searchQuery: newValue => this.updateAutocomplete(newValue))。理由是箭头函数绑定了父级作用域的上下文,所以 this 将不会按照期望指向 Vue 实例,this.updateAutocomplete 将是 undefined。(官方文档:watch)
使用深度监听后, 提交表单数据时对数据进行处理,而页面竟然也更新了
举个栗子
设置运费模板时, 因为接口处理是按分为单位的, 而用户输入的是元, 中间需要做转换,由于使用了watch监听, 在处理传参时页面数据竟然也被修改了,代码如下:
//formItem数据结构如下:
forItem:{
freightList:[{
initFreight: 10,
addFreight: 10,
provinceIdList:[],
}]
...
}
//watch监听:
forItem: {
handler(newVal, oldVal){
console.log(this.formItem)
},
deep: true,
immediate: true
},
在提交时设置initFreight*100之后, 页面上显示的数据也变成了1000。我试图把formItem赋值给newForm, 可改变newForm中的initFreight时,页面依然会改变。
这是为什么呢?
原因是把formItem赋值给newForm后, 它们指向的是同一个地址,即值引用。
newForm = JSON.parse(JSON.stringify(this.forItem));
只需要使用JSON转义就可以解决问题了。v-model出现这个问题也是同样的解决方法。
推荐文档:
对vue响应式数据更新的误解
watch源码
深入理解Vue的手表实现原理及其实现方式
搞懂computed和watch原理,减少使用场景思考时间
Watch - 源码解析