Skip to content
On this page

Vue 中 对象 和 数组 的修改

对于Vue的vm实例,提供了 vm.$setvm.$del 的方法修改 对象类型 的数据,数组通过原型链改写来修改了数组的原生方法来适应vm实例的数组修改

$set 方法的实现

js
// obsrver/index.js
function set (target, key, val) {
  if (isUndef(target) || isPrimitive(target)) {
    // target 是undefined或者非Object/Array类型
  }
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    // 数组类型判断:修改的序号是合法的序号
    target.length = Math.max(target.lengt, key)
    target.splice(key, 1, val) // 利用数组splice方法,这里是经过原型链改写的方法
    return val
  }
  if (key in target && !(key in Object.prototype)) {
    // key是修改对象上的y属性
    target[key] = val
    return val
  }
  const ob = target.__ob__
  if (target._isVue || (ob && ob.vmCount)) {
    // 不能直接修改 vm 实例的属性,也不能直接增加vm实例上的rootData里的数据
    return val
  }
  if (!ob) {
    target[key] = val
    return val
  }
  // set进去的值也变成响应式数据
  // observer实例里的value存储了值
  defineReactive(ob.value, key, val)
  // 每个observer会有一个dep实例,手动通知渲染watcher(或者其他watcher)重新渲染
  // 之前setter函数也会调用dep.notify()
  ob.dep.notify()
  return val
}

总结:因为有这个set方法,所以我们可以使用 this.$set(obj, key, val) 和 this.$set(arr, index, val),在set方法的时候,会手动触发 dep实例的 notify 方法来通知watcher更新

数组Array类型的原型链改写

在设立 Array 类型响应式数据的时候,会在array内增加一个observer实例

js
// observer/index.js
const arrayProto = Array.prototype
const arrayMethods = Object.create(arrayProto)
const hasProto = '__proto__' in {}

class Observer {
  constructor (value) {
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
      // 数组类型判断
      // 是否支持__proto__ 这个属性
      if (hasProto) {
        // 改写原型链
        protoAugment(value, arrayMethods)
      } else {
        copyAugment(value, arrayMethods, arrayKeys)
      }
    }
  }
}

function protoAugment (target, src) {
  target.__proto__ = src
  // 将数组的原型改为创建出来的Array对象,在调用数组修改方法的时候就会先找到这个新原型上的方法
}

数组的新原型 arrayMethods

从上面可以知道,数组Array类型的数据 会被 重新指定原型 arrayMethods

js
const arrayProto = Array.prototype
const arrayMethods = Object.create(arrayProto)
const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]
methodsToPatch.forEach(function (method) {
  // 先缓存原方法
  const original = arrayProto[method]
  def(arrayMethods, method, function mutator (...args) {
    // 同样是利用defineProperty来定义这个新原型上的方法,不可枚举
    // 先执行原有方法
    const result = original.apply(this, args)
    const ob = this.__ob__ // 数组内的observer实例
    let inserted
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    // 数组长度经过上面三个操作的话会发生数组长度的变化,有新元素插入或者旧元素被剔除
    if (inserted) ob.observeArray(inserted)
    // 手动通知数组的依赖项
    ob.dep.notify()
    return result
  })
})


数组通过原型链修改的方法,会先调用原生数组方法,然后通过之前设置响应式数据时,在数组内部添加的ob实例,手动通知这个数组的订阅者watcher

$del方法的实现

js
// observer/index.js
function del (target, key) {
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    // 数组类型直接调用splice,经过新的原型会手动通知数组的订阅者
    target.splice(key, 1)
    return;
  }
  const ob = target.__ob__
  if (target._isVue || (ob && ob.vmCount)) {
    // vm实例属性 或者 vm实例根data不可修改
    return;
  }
  if (!hasOwn(target, key)) {
    return // target内压根原来就没有key这个属性
  }
  delete target[key] // 直接删除属性
  if (!ob) return
  ob.dep.notify() // 手动通知对象的订阅者
}

del的逻辑跟set类似,都是根据不同的数据类型,调用不同的逻辑,最后都会手动调用它们内部的ob实例来更新watcher

ob实例内的watcher是什么时候收集的

在使用defineReactive定义响应式对象的时候,会有如下逻辑来完成内部的响应式设置

js
// observer/index.js
function defineReactive (obj, key, val, customSetter, shallow) {
  let childOb = !shallow && observer(val) // 传入的值是对象或数组,递归观察,返回子ob
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        dep.depend() // 依赖收集
        if (childOb) {
          childOb.dep.depend() // 子ob在getter函数进行收集依赖
          if (Array.isArray(value)) {
            // 对于数组的话,数组内每一项都收集这个依赖
            dependArray(value)
          }
        }
      }
      return value
    },
  })
}

总结,当设置对象类型或者数组类型的响应式数据时,会递归调用observe来观察其子项,设置子ob实例,当用上面那些方法更改时,就出触发ob实例内部的dep.notify()

比如之前执行 observe(data) 的时候,不仅会在根data内设立ob实例,每个data内的属性数据设置响应式,如果该数据还是对象或者数组,会递归observe,生成子ob,依次类推