源码分析:Vue响应式动态添加对象或数组属性的问题 Vue.set原理

352 阅读2分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

响应式数据中存在的问题

关于Vue2.x数据响应式有一些值得注意的点,这些点在日常的开发过程中,很可能会遇到,了解原理有助于我们对这些情况的处理。

1.关于响应式对象

如下代码,当我们在Vue组件中动态新增对象obj上的属性msg2,这样的情况下定义的数据是非响应式的,只有在data中初始化的时候定义的数据(如msg1)才是响应式的。这是为什么呢?在之前的文章中,我们分析过数据是如何被响应式的,在initData的过程中,定义在data中的对象会被依次遍历,执行observe方法,当对象是个深层次的对象的时候,会递归执行observe方法,直到对象上所有的属性被访问到。而未在data中定义的属性(比如下面例子中的msg2)在数据的init的过程中,就无法访问到来做响应式的处理。

export default {
  data() {
    return {
      obj: {
        // msg1是响应式数据
        msg1: 'hello world',
      }
    }
  }
  created() {
    // 这样定义的msg2是非响应式数据
    this.obj.msg2 = 'hello Vue'
  }
}

那在实际开发过程中,我们需要给对象动态添加响应式属性,该怎么做呢,Vue的官方提供了一个API

Vue.set( target, propertyName/index, value )

我们看下这个API的源码:

/**
 * Set a property on an object. Adds the new property and
 * triggers change notification if the property doesn't
 * already exist.
 */
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)}`)
  }
  // target是数组的话,且key是个有效的index,直接通过调用splice方法去添加进数组然后返回这个值,这个splice方法已经改写过了
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.length = Math.max(target.length, key)
    target.splice(key, 1, val)
    return val
  }
  // target 是普通对象的情况下,执行下面的逻辑
  // key 已经存在于对象上且非原型链上查找的值,直接将对象上的值修改为传入的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
  }
  // target.__ob__不存在的话,说明不是响应式的对象,直接返回
  if (!ob) {
    target[key] = val
    return val
  }
  // 以上条件都满足,调用defineReactive,对新属性进行响应式处理
  defineReactive(ob.value, key, val)
  // 通知与ob相关的依赖做更新
  ob.dep.notify()
  return val
}

2.关于响应式数组

关于数组,Vue 也是不能检测到以下动态变化的数组:

// 通过下标修改值
vm.arr[index] = newValue
// 修改数组长度
vm.arr.length = newLength

对于通过下标修改值的情况,可以使用Vue.set方法:

Vue.set(arr, index, newValue)
// target是数组的话,且key是个有效的index,直接通过调用splice方法去添加进数组然后返回这个值
if (Array.isArray(target) && isValidArrayIndex(key)) {
  target.length = Math.max(target.length, key)
  target.splice(key, 1, val)
  return val
}

这里的splice已经被重写过了,不清楚这段逻辑可以点击这里,splice会遍历数组中的值,做响应式处理,并通知数组的依赖做更新。

关于修改数组长度,可以使用splice方法:

arr.splice(newLength)