从源码解惑,为什么明明修改了数据视图却不更新

1,286 阅读3分钟

问题描述

在使用Vue日常工作开发中,偶尔会遇到这种问题,明明我已经修改了数据,但是视图却没有更新。比如下面这些骚操作:

  • 直接在对象上新增属性
  • 直接删除对象属性
  • 通过角标[]直接修改数组的某一项
  • 直接修改数组的length

问题分析

想要了解为什么上面这些写法不会触发视图更新,只需要搞清楚在vue中是如何对数据进行响应式处理的。知道了vue的数据响应机制,那么跳出机制的写法自然就不能触发视图更新了。
在vue中,对于对象和数组会进行不同的响应式处理

if (Array.isArray(value)) {
  if (hasProto) {
    protoAugment(value, arrayMethods)
  } else {
    copyAugment(value, arrayMethods, arrayKeys)
  }
  this.observeArray(value)
} else {
  this.walk(value)
}

对象

如果是对象,会在组件初始化的时候通过Object.defineProperty对每个属性进行响应式处理,但是这个过程是一次性的,说白了就是过了这个村就没这个店了,没上车的就只能吸着尾气目送队友离开

 /**
   * Walk through all properties and convert them into
   * getter/setters. This method should only be called when
   * value type is Object.
   */
  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }

pic4
我们后来手动新增和删除的属性并不是响应式的,也就不会触发视图更新。

数组

如果是数组,则会修改数据的__proto__属性

/**
 * Augment a target Object or Array by intercepting
 * the prototype chain using __proto__
 */
function protoAugment (target, src: Object) {
  target.__proto__ = src
}

这里的src根据上面的代码看到传入的是arrayMethods, 这个arrayMethods定义如下:

const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)

然后对七个能改变数组的方法进行拦截,当我们调用这七个方法修改数组的时候,会先执行原始方法,然后dep通知watcher更新依赖

const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]

/**
 * Intercept mutating methods and emit events
 */
methodsToPatch.forEach(function (method) {
  // cache original method
  const original = arrayProto[method]
  def(arrayMethods, method, function mutator (...args) {
    const result = original.apply(this, args)
    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)
    // notify change
    // 关键点在这,通知依赖更新
    ob.dep.notify()
    return result
  })
})

pic
我们使用角标修改数组和更改数组的length都不在上面的机制之内,所以数据改变视图也不会更新

如何解决

两种方法

  1. 调用vm.$forceUpdate方法强制重新渲染组件
    如果你一定要用上面那些写法,那么可以调用这个方法强行使组件重新渲染,保证展示最新修改的数据
  2. 使用Vue.setVue.delete为数据新增或删除属性/子项(调用vm.$set和vm.$delete原理相同)
Vue.set = set
Vue.delete = del
export function set (target: Array<any> | Object, key: any, val: any): any {
  // 此处省略一万行代码。。。
  
  // 为新增的属性定义响应式
  defineReactive(ob.value, key, val)
  // 通知依赖更新
  ob.dep.notify()
  return val
}
export function del (target: Array<any> | Object, key: any) {
  // 此处省略一万行代码。。。
  if (!hasOwn(target, key)) {
    return
  }
  delete target[key]
  if (!ob) {
    return
  }
  // 通知依赖更新
  ob.dep.notify()
}

这两个方法都很好理解,核心就是dep通知watcher更新外界依赖

结论

出现视图不更新的情况,基本都是做了一些跳出了Vue数据响应机制之外的骚操作,大家在工作开发中还是应该避免使用这些写法。另外,官方也不推荐使用Vue.set和Vue.delete方法,提供这两个方法也只是无奈之举,对于数据我们还是统一维护比较好。