Vue源码解读 | vue 数据更新却不render?

3,681 阅读2分钟

以下都基于数据已经渲染到 dom 上后再对数据进行修改,console 出来的数据更新了,但绑定的 dom 不更新的问题

1. 更新对象的属性不render

data() {
    return {
        detail: {}
    }
}
created() {
    this.detail = {
        a: '1', // 更新
        b: '2'  // 更新
    }
}
mounted () {
    this.detail.c = '12'  // 不更新
}

vue 不允许动态添加对象的根级别属性

Vue 会在初始化实例时对对象的属性执行 getter/setter 转化,所以属性必须在 data 对象上存在才能让 Vue 将它转换为响应式的

解决方法:使用 $set 给对象添加属性或在初始化对象时就声明属性

// 方法一
this.$set(this.detail, 'c', 12)

// 方法二

this.detail = {
    a: '1',
    b: '2',
    c: '12'
}

2. 更新数组数据不render?

// 场景1
export default {
    data () {
    return {
      detail: []
    }
  },
  created () {
    this.detail[0] = { a: 2 }
  },
  mounted () {
    this.detail[0].a = 4 // 不更新
  }
}

// 场景2
export default {
    data () {
    return {
      detail: []
    }
  },
  created () {
    this.detail[0] = 2
  },
  mounted () {
    this.detail[0] = 3 // 不更新
  }
}

数组的索引是没有响应式的,比如上面的 detail 的 0 这个位置是没有 setter/getter 的,所以无法检测到该数据的变更,{ a: 2 } 直接赋值给了 0 的位置,所以也无法对 a 做响应式转化

解决方法: 同样可以使用 $set 给数组的索引的内容执行 getter/setter 转化,但也可以使用 变异方法

使用 $set 设置数组指定位置的数据

this.$set(this.detail, 0, { a: 2 })

变异方法:

  • push()
  • pop()
  • splice()
  • shift()
  • unshift()
  • sort()
  • reverse()
export default {
    data () {
    return {
      detail: []
    }
  },
  created () {
    this.detail.splice(0, 1, { a: 2 })
  },
  mounted () {
    this.detail[0].a = 4 // 更新
  }
}

还有一种方式,就是给数组重新赋值

this.detail = [{a: 2}]

源码里,其实在 $set 处理数组时,内部也是通过 splice() 对数组的元素进行操作的

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
  }
  // 响应式转化: 给对象属性执行 setter/getter 转化
  defineReactive(ob.value, key, val)
  ob.dep.notify()
  return val
}

3. 为什么变异方法就能让新增的数据有响应式呢?

这个官方文档并没有细说,但是翻过源码的人就能知道,为什么上面这些数组的原生方法被叫做变异方法呢,字面上了解就是 vue 对原生的这些方法做了一点点的修饰。

// /core/observer/array.js
const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]
// 通过def 重新定义了数组的原生函数如push等
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
  })
  
  observeArray (items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
    // 遍历参数配置响应式
      observe(items[i])
    }
  }