Vue为什么不能检测数组变动

Vue检测数据的变动是通过Object.defineProperty实现的,所以无法监听数组的添加操作是可以理解的,因为是在构造函数中就已经为所有属性做了这个检测绑定操作。
但是官方的原文:由于 JavaScript 的限制, Vue 不能检测以下变动的数组:
当你利用索引直接设置一个项时,例如: vm.items[indexOfItem] = newValue
当你修改数组的长度时,例如: vm.items.length = newLength

这句话是什么意思?我测试了下Object.defineProperty是可以通过索引属性来设置属性的访问器属性的,那为何做不了监听?
有些论坛上的人说因为数组长度是可变的,即使长度为5,但是未必有索引4,我就想问问这个答案哪里来的,修改length,新增的元素会被添加到最后,它的值为undefined,通过索引一样可以获取他们的值,怎么就叫做“未必有索引4”了呢?

既然知道数组的长度为何不能遍历所有元素并通过索引这个属性全部添加set和get不就可以同时更新视图了吗?
如果非要说的话,考虑到性能的问题,假设元素内容只有4个有意义的值,但是长度确实1000,我们不可能为1000个元素做检测操作。但是官方说的由于JS限制,我想知道这个限制是什么内容?各位大大帮我解决下这个问题,感谢万分

阅读 5.7k
评论
    7 个回答

    我最近刚好写过 Vue 的 Observer 部分源码分析博客
    强答一波...

    并不是说 JS 不能支持响应式数组,没有这种限制,而是一般开发者使用数组与使用对象的方法有区别。数组在 JS 中常被当作栈,队列,集合等数据结构的实现方式,会储存批量的数据以待遍历。并且 runtime 对对象与数组的优化也有所不同。

    所以对数组的处理需要特化出来以提高性能。

    Vue 中是通过对每个键设置 getter/setter 来实现响应式的,开发者使用数组,目的往往是遍历,此时调用 getter 开销太大了,所以 Vue 不在数组每个键上设置,而是在数组上定义 __ob__ ,并且替换了 push 等等能够影响原数组的原型方法。

    至于 length 的问题,你用 Object.keys() 或者 Object.getOwnPropertyNames() 就能获得所有键的名字,前者是所有自有可枚举的,后者是所有自有的,不需要用 length 。

    从源码可以清晰看到,Vue 跳过了对数组每个键设置响应式的过程,而是直接对值进行递归设置响应式

    export class Observer {
      constructor(value: any) {
        def(value, "__ob__", this);
        if (Array.isArray(value)) {
          // 替换原型(Object.setPrototype 这个方法执行地比较慢,而且支持情况堪忧)
          Object.setPrototypeOf(value, arrayMethods);
          this.observeArray(value);
        } else {
          this.walk(value);
        }
      }
      public walk(value: any) {
        for (const key of Object.keys(value)) {
          defineReactive(value, key);
        }
      }
      public observeArray(items: any[]) {
        // 设置 l = items.length 防止遍历过程中 items 长度变化
        for (let i = 0, l = items.length; i < l; i++) {
          // 直接观察数组元素,省略在键上设置 getter/setter 的步骤
          observe(items[i]);
        }
      }
    }

    而且楼主你造吗?Vue 使用 $watch('arr') 是可以监听多维数组的,Vue 对普通对象和数组的设置情况是差异很大的。

    另外还有就是楼上有人贴图
    clipboard.png
    这里面的$set支持设置数组+响应式非数字键,Vue确实是这么支持的,然而!!如果在初始化时data里这么干,Vue会忽略非数字键的,不会变响应式,它这是行为不一致!对它使用watch,直接修改它不会变,但修改数组就会触发。

    我早给Vue提issue了,但他们一直不理我。
    https://github.com/vuejs/vue/...

    欢迎阅读我博客Vue Observer源码分析,写得非常详细。
    https://segmentfault.com/a/11...

      • 12.2k
        • 2.5k

        你可以先按你的想法实现一个可以监控的数组 而且数组是可以arr[5]=xxx这种操作 所以这种操作也要被监控到

          • 1.6k

          因为数组不方便拓展
          你既然测试了,应该发现这个问题了啊

          let list = []
          
          Object.defineProperty(list, 0, {
            get(){
              console.log('get 0')
              // 如果是对象,可以给对象拓展一个$data,用来存放真正的数据,但数组不能.
              // 只能放在别的地方,那显然不科学,不合理.
              // 如果直接 return this[0],就递归了,爆栈
            },
            set (v) {
              console.log('set o', v)
          // 直接 this[0] = v,同样爆栈
            }
          })

          那就只能看不能用了

              所以题主,你现在搞懂为什么了嘛?
              字最多的那位仁兄,你的意思是说,本来Object.defineproperty()是可以劫持数组的,但是这样开销很大,所以没有使用Object.defineproperty(),而是在数组原型上添加方法对数组进行响应式修改,对吗?
              @undefinedvar @xyzinsf

                • 1.1k

                vue不能检测数组改变 vue提供了一个set方法

                clipboard.png

                  撰写回答

                  登录后参与交流、获取后续更新提醒