响应式相关的部分方法

292 阅读3分钟

本节简单看一下 Vue.set() 以及数组的方法

通过前面几节简述我们已经了解响应式对象以及依赖收集、派发通知的过程,但是对于一些特殊的情况我们还是需要再去关注一下的。


首先是关于直接为一个对象添加属性,为什么不会触发页面重新渲染?

export default {
    data() {
        return {
            msg: { a: 1 }
        }
    },
    mounted() {
        this.msg.b = 2;
    }
}

例如上述代码,我们已经在组件定义了一个数据对象 msg: {a:1} ,在 mounted() 函数内为其新添加属性,为什么不会触发页面重新渲染呢?

1、首先在初始化时会将数据对象转为响应式数据,在此过程中会为每个对象新增 __ob__ 属性且该属性持有一个 dep 实例,还会遍历对象的键 key 让其也持有一个 dep 实例,以进行依赖收集。当数据对象的键 key 对应的值发生变化时,会派发通知。

2、由上述代码可知,我们是为 msg 对象新增一个属性并赋值。由于在初始化时并没有这个键,所以没有进行依赖收集,所以此次改变也不会触发依赖更新。

有人会有疑问,说对象 msg.__ob__ 属性也持有一个 dep 实例并且初始化时也进行了依赖收集会什么不会触发重新渲染呢?呢是因为从源码角度看,派发通知是从键 key 的角度触发的,如果初始化时该 key 没有,呢么后续确实不会触发依赖通知。

官方给出使用 Vue.set(this.msg, b, 2); 才会触发页面重新渲染。接下来我们一起看看该方法式如何实现的。如下:

function set(target, key, val) {
  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.__ob__
  if (!ob) {
    target[key] = val
    return val
  }
  defineReactive(ob.value, key, val)
  ob.dep.notify()
  return val
}

Vue.set(target,key,val) 函数的实现还是比较简单的。

  • target 为数组并且 key 为合法索引,则通过 target.splice() 函数直接修改对应位置的值并返回 val
  • keytarget 对象的自身属性并非原型属性,则赋值 target[key] = val;
  • 获取到对象的重要属性 ob = target.__ob__ 该属性用来区分对象是否为响应式对象。
  • ob 不存在,则 target 为非响应式对象,则赋值 target[key] = val;
  • 最后通过 defineReactive() 函数将 key、val 添加到对象上并设置为响应式数据。
  • 再手动触发通知 ob.dep.notify(); 触发页面重新渲染。因为该 ob 实例也持有 dep 实例并进行了依赖收集,所以可以手动进行派发通知。

其次,很多时候我们直接新增/删除数组项也可以触发重新渲染,这又是怎么实现的呢?

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) {
    // 先执行原方法获取结果
    const result = original.apply(this, args)
    // 获取当前数组的响应式标志属性 __ob__
    const ob = this.__ob__
    // 缓存参数
    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
  })
})

def (obj, key, val, enumerable = false) {
  Object.defineProperty(obj, key, {
    value: val,
    enumerable: !!enumerable,    // 默认不能枚举
    writable: true,
    configurable: true
  })
}

由上述代码可知在 Vue 框架中,重新定义了可以修改数组长度、数组顺序的七个方法。该实现也是相对比较简单一些,下面重点看一下!

先缓存数组原方法,然后通过 Object.defineProperty() 函数重新在数组上定义该函数。在该函数内部:

  • 首先执行数组原方法获取结果 result
  • 再获取数组的响应式标志属性 ob = this.__ob__
  • 再获取数组新增的参数,再尝试将参数转为响应式数据
  • 手动触发通知 ob.dep.notify() 进行重新渲染
  • 最后返回结果 result