Vue2.0 数组的监控 - 图1

常见面试题

  • Vue 如何监控数组
  • defineProperty 真的不能监测数组变化吗?

Vue 是如何追踪数据发生变化

在 Vue 中当我们把一个普通的 JS 对象作为 data 传入 Vue 实例,Vue2.x 对这个数据初始化时将遍历这个对象所有的属性,并使用 JS 的原生特性 Object.defineProperty 把这些属性全部转为 getter\setter。这些 getter\setter 对用户来说是不可见的,他们可以在属性被访问和修改时通知变更。同时每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把“接触”过的数据属性记录为依赖。之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染。
image.png

Vue 如何更新数组

  1. // 方法一: 使用 Vue.set
  2. Vue.set(vm.items, indexOfItem, newValue)
  3. // 方法二: 使用 Vue 可监测的数组变异方法: Array.prototype.splice
  4. vm.items.splice(indexOfItem, 1, newValue)

为什么有些数组的数据变更不能被 Vue 监测到

简单来说,我们操作数组的一些动作 arr[2] = ‘xxx’ / arr.length = 2 或者是调用 Array.prototype 上挂载的部分方法并不能触发这个属性的 setter。

在数组的更新中有提到,可以使用 Vue 可监测的数组变异方法: Array.prototype.splice,哪为什么这个方法可以触发状态的更新了。这是因为 Vue2.x 将数组的 7 个常用方法 push、pop、shift、unshift、splice、sort、reverse 进行了重写,所以通过调用包装之后的数组方法就能够被 Vue 监测到。

  1. // Vue 2.6.14
  2. // src/core/observer/array.js
  3. import { def } from '../util/index'
  4. // 记录原始 Array 未重写之前的 API 原型方法
  5. const arrayProto = Array.prototype
  6. // 深拷贝一份上面的原型出来
  7. export const arrayMethods = Object.create(arrayProto)
  8. const methodsToPatch = [
  9. 'push',
  10. 'pop',
  11. 'shift',
  12. 'unshift',
  13. 'splice',
  14. 'sort',
  15. 'reverse'
  16. ]
  17. /**
  18. * Intercept mutating methods and emit events
  19. * 拦截上边数组中列出的变异方法, 并发出事件通知
  20. */
  21. methodsToPatch.forEach(function (method) {
  22. // cache original method
  23. // 缓存 Array.prototype 中的同名原始方法
  24. const original = arrayProto[method]
  25. def(arrayMethods, method, function mutator (...args) {
  26. // 调用执行原有的数组方法
  27. const result = original.apply(this, args)
  28. const ob = this.__ob__
  29. let inserted
  30. switch (method) {
  31. case 'push':
  32. case 'unshift':
  33. inserted = args
  34. break
  35. case 'splice':
  36. inserted = args.slice(2)
  37. break
  38. }
  39. // 如果是插入的数据,将它再次监听起来
  40. if (inserted) ob.observeArray(inserted)
  41. // 触发订阅,像页面更新响应就在这里触发
  42. ob.dep.notify()
  43. return result
  44. })
  45. })

Vue 为什么不能通过下标操作数组或者改变数组的长度来触发视图更新

那 Vue2.x 监测数组变更的两条限制:不能监听利用索引直接设置一个数组项,不能监听直接修改数组的长度,是因为 defineProperty 的限制么?

答案:是的

Object.defineProperty 对于数组变化监听的表现与 Vue2.x 还是有不同的,比如 Object.defineProperty 可以监听到通过索引直接修改数组项,当然也不是说 Object.defineProperty 可以完全监听数组的变化,像直接修改数组的长度或者 push\pop 之类的方法还是不能触发 setter 的。

这里就是出现一个新的问题?

为什么 Object.defineProperty 明明能监听到数组值的变化,而 Vue 却没有实现呢?

这是因为 Vue 是对数组元素进行了监听,而没有对数组本身的变化进行监听。

  1. var Observer = function Observer (value) {
  2. this.value = value;
  3. this.dep = new Dep();
  4. this.vmCount = 0;
  5. def(value, '__ob__', this);
  6. // 区分对象和数组,对象和数组走不通的响应式方案
  7. if (Array.isArray(value)) {
  8. // 判断是否支持__proto__属性,根据不同的请求来添加数组的拦截方法
  9. if (hasProto) {
  10. protoAugment(value, arrayMethods);
  11. } else {
  12. copyAugment(value, arrayMethods, arrayKeys);
  13. }
  14. // 循环数组的元素,再次调用observe方法,
  15. this.observeArray(value);
  16. } else {
  17. // 如果是对象,循环对象属性,为对象属性添加getter,setter方法,将属性变成响应式
  18. this.walk(value);
  19. }
  20. };

这其实是出于性能原因的考量,给每一个数组元素绑定上监听,实际消耗很大而受益并不大

其实还有一些考虑是:对数据的操作更常用的操作数组的方法是使用数组原型上的一些方法如 push、shift 等来操作数组。Object.defineProperty是对象上的方法,用来对数组的下标进行检测,会改变数据本来的性质。

总结来说:三点原因

  • 性能原因的考量
  • 对数据的操作更常用的操作数组的方法是使用数组原型上的一些方法如 push、shift 等来操作数组。
  • Object.defineProperty是对象上的方法,用来对数组的下标进行检测,会改变数据本来的性质。

    Vue 3.0 是如何处理的?

    Vue3 不再采用 defineProperty 的方式来进行监听而是采用 Proxy 的方式。下面我引用了 MDN 上对于 proxy 的介绍:

    Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。

当异步触发 Model 里的数据变化时,都会经过 Proxy 这一层,在这里则可以监听数组以及各种数据类型的变化,无论是数组下标赋值引起变化还是数组方法引起变化,都可以被监听到,也可以避开监听数组每个属性下造成的性能问题。