为什么你的watch不生效? 从内部实现解析watch的工作原理

4,624 阅读5分钟

前言

使用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: {
        typeObject,
        requiredtrue
    }
},
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:[{
    initFreight10,
    addFreight10,
    provinceIdList:[],
  }]
  ...
}
//watch监听:
forItem: {
  handler(newVal, oldVal){
    console.log(this.formItem)
  },
  deeptrue,
  immediatetrue
},

在提交时设置initFreight*100之后, 页面上显示的数据也变成了1000。我试图把formItem赋值给newForm, 可改变newForm中的initFreight时,页面依然会改变。
这是为什么呢?
原因是把formItem赋值给newForm后, 它们指向的是同一个地址,即值引用。

newForm = JSON.parse(JSON.stringify(this.forItem));

只需要使用JSON转义就可以解决问题了。v-model出现这个问题也是同样的解决方法。

推荐文档:
对vue响应式数据更新的误解
watch源码
深入理解Vue的手表实现原理及其实现方式
搞懂computed和watch原理,减少使用场景思考时间