vue中this.$set()原理

在vue开发中,经常会遇到这样的问题:当我们给响应式的对象新增属性时,新增的属性并不会显示到页面中;同样的对于响应式的数组,增加元素、修改数组长度时,数组的这些变化也不会反映到页面中(响应式对象和响应式数组是指在vue初始化时期,利用Object.defineProperty()方法对其进行监听,这样在修改数据时会及时体现在页面上)。如下所示:

<div id="app">
  {{ obj }}
  {{ arr }}
</div>
let vm = new Vue({
  el: '#app',
  data() {
    return {
      obj: {
        a: '哈哈哈',
      },
      arr: [1, 3]
    }
  },
  mounted() {
    this.changeObj()
    this.changeArr()
  },
  methods: {
    changeObj() {
      this.obj.b = '新增的属性b'
      console.log(this.obj)
    },
    changeArr() {
      this.arr[4] = 7
      this.arr.length = 5
      console.log(this.arr)
    }
  }
})
vm.$data.obj.c = '新增属性c'
vm.$data.arr[2] = 5

从上面例子中可以看出在页面加载后,我们通过this.obj.b新增属性b时,页面中并没有将属性b显示出来;通过vm.$data.obj.c新增属性c时,页面中也没有将属性c显示出来;但是通过打印对象obj,可以看出obj上存在属性b和c,为什么呢?

这是由于对象obj及其属性a在vue初始化时已处理成响应式的,即当我们改变对象obj的值或属性a的值时,会触发其在页面上的更新,但是当页面加载完成后,新增的属性b和c就不是响应式的,虽然通过打印this.obj可以看出对象obj上确实增加了属性b和c,但是由于b和c不是响应式的,所以新增的属性不会体现在页面上。

对于数组arr也存在类似的问题,当我们通过索引值添加元素或更改数组长度时,数组本身是发生变化了,但是没有及时体现在页面上。

对于上述问题,我们通常使用vue中的this.$set()来解决,那么this.$set()的原理是什么呢?

在使用this.$set(target, key, value)时,target为需要添加属性的对象,key是要添加的属性名,value为属性key对应的值。

点击查看vue中set源码

// src/core/observer/index.js
export function set (target: Array<any> | Object, key: any, val: any): any {
  if (process.env.NODE_ENV !== 'production' &&
    (isUndef(target) || isPrimitive(target))
  ) {
    warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)
  }
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.length = Math.max(target.length, key)
    target.splice(key, 1, val)
    return val
  }
  if (key in target && !(key in Object.prototype)) {
    target[key] = val
    return val
  }
  const ob = (target: any).__ob__
  if (target._isVue || (ob && ob.vmCount)) {
    process.env.NODE_ENV !== 'production' && warn(
      'Avoid adding reactive properties to a Vue instance or its root $data ' +
      'at runtime - declare it upfront in the data option.'
    )
    return val
  }
  if (!ob) {
    target[key] = val
    return val
  }
  defineReactive(ob.value, key, val)
  ob.dep.notify()
  return val
}

上面源码的执行逻辑如下:

1、如果是在开发环境,且target未定义(为null、undefined)或target为基础数据类型(string、boolean、number、symbol)时,抛出告警;

2、如果target为数组且key为有效的数组key时,将数组的长度设置为target.length和key中的最大的那一个,然后调用数组的splice方法(vue中重写的splice方法)添加元素;

3、如果属性key存在于target对象中且key不是Object.prototype上的属性时,表明这是在修改target对象属性key的值(不管target对象是否是响应式的,只要key存在于target对象中,就执行这一步逻辑),此时就直接将value直接赋值给target[key];

4、判断target,当target为vue实例或根数据data对象时,在开发环境下抛错;

5、当一个数据为响应式时,vue会给该数据添加一个__ob__属性,因此可以通过判断target对象是否存在__ob__属性来判断target是否是响应式数据,当target是非响应式数据时,我们就按照普通对象添加属性的方式来处理;当target对象是响应式数据时,我们将target的属性key也设置为响应式并手动触发通知其属性值的更新;

上面代码中最重要的就是如下代码:

defineReactive(ob.value, key, val)
ob.dep.notify()

这两行是将新增属性设置为响应式,并手动触发通知该属性值的更新,这就是通过this.$set()设置之后新增的属性会变成响应式并及时体现在页面中的原因。

参考文献:

[1] 从vue源码看Vue.set()和this.$set()

Logo

前往低代码交流专区

更多推荐