likes
comments
collection
share

Vue为何不能监听数组的变化 如 this.arr[1]='abc'

作者站长头像
站长
· 阅读数 25

Vue为何不能监听数组的变化 如 this.arr[1]='abc'

vue 2.0底层监听用的是 Object.defineProperty api。我们首先来看下官方文档对于这个api的一个说明 developer.mozilla.org/zh-CN/docs/…

我们看到官方给出的demo都是对象监听的,不过我们通过代码测试确实defineProperty是可以监听数组变动。

      function newDef(obj, key, describe) {
        Object.defineProperty(obj, key, {
          // value: val, //值
          enumerable: true, //定义了对象的属性是否可以在 for...in 循环和 Object.keys() 中被枚举。
          // writable: true, //可以 改写 value
          configurable: true, //configurable特性表示对象的属性是否可以被删除,以及除writable特性外的其他特性是否可以被修改。
          ...describe,
        });
      }

      let arr = [1, 2, 3];
      newDef(arr, "0", {
        get: () => {
          console.log("get=");
        },
        set: (v) => {
            
          console.log("set arr=");
        },
      });
      arr[0] = 99;

日志可以看到进入了 "set arr="。 所以vue2 从技术上是能做到 监听数组索引数据改变的,那为什么vue2 并没有这么做呢?

首先我们来测试下vue2 如果需要做 defineProperty 监听数组索引下数值变化要怎么做呢?其实很简单。我们来修改下代码

   *
   * 实例化 dep对象,获取dep对象  为 value添加__ob__ 属性
   */
  var Observer = function Observer(value) {
  // var Observer = function Observer(value) {
    console.log('value=======',value)
    debugger
    this.value = value;
    this.dep = new Dep();
    this.vmCount = 0;
    //设置监听 value 必须是对象
    def(value, "__ob__", this);
    if (Array.isArray(value)) { //如果是数组 是需要转换 过滤掉数组索引
      //判断是不是数组
      var augment = hasProto //__proto__ 
        ?
        protoAugment :
        copyAugment;
      augment(value, arrayMethods, arrayKeys);
      this.observeArray(value);
    } else {
      this.walk(value);
    }
  };

vue 对于数组数据是做了特殊判断了 会进过一个 observeArray 方法 这个方法我们来看看

  /**
   * Observe a list of Array items.
   * 观察数组项的列表。
   * 把数组拆分一个个 添加到观察者 上面去
   */
  Observer.prototype.observeArray = function observeArray(items) {
    for (var i = 0, l = items.length; i < l; i++) {
      console.log("items[i]");
      console.log(items[i]);

      observe(items[i]);
    }
  };

他会把这个数组拆分在加入监听 ,其实讲直白点就是会丢弃了 数组的索引key然后里面数组深层的数据在加入监听。 那我们要vue去监听数组索引数值变化 其实很简单 就是不要让数组特殊处理,而是让他也直接加入监听 修改代码如下

  var Observer = function Observer(value) {
 // var Observer = function Observer(value) {
   console.log('value=======',value)
   debugger
   this.value = value;
   this.dep = new Dep();
   this.vmCount = 0;
   //设置监听 value 必须是对象
   def(value, "__ob__", this);

   // 不管数组还是对象直接加入监听
   this.walk(value);
   // 注释掉
   // if (Array.isArray(value)) { //如果是数组 是需要转换 过滤掉数组索引
   //   //判断是不是数组
   //   var augment = hasProto //__proto__ 
   //     ?
   //     protoAugment :
   //     copyAugment;
   //   augment(value, arrayMethods, arrayKeys);
   //   this.observeArray(value); 
   // } else {
   //   this.walk(value);
   // }
 };

然后看看 walk 方法

  /**
  * Walk through each property and convert them into
  * getter/setters. This method should only be called when
  * value type is Object.
  * *遍历每个属性并将其转换为
  * getter / setter。此方法只应在调用时调用
  *值类型是Object。
  */
 Observer.prototype.walk = function walk(obj) {
   var keys = Object.keys(obj);
   for (var i = 0; i < keys.length; i++) {
     // 
     defineReactive(obj, keys[i]);
   }
 };

在看看defineReactive 方法。这个方法就是vue所有数据监听的一个方法入口 包括 data 数据 还有动态的 props 还有一些 watch,computed 计算属性等都会走这个方法加入监听。


  /**
  * Define a reactive property on an Object.
  * 在对象上定义一个无功属性。
  * 更新数据
  * 通过defineProperty的set方法去通知notify()订阅者subscribers有新的值修改
  * 添加观察者 get set方法
  */
 function defineReactive(
   obj, //对象
   key, //对象的key
   val, //监听的数据 返回的数据
   customSetter, //  日志函数
   shallow //是否要添加__ob__ 属性
 ) {
   //实例化一个主题对象,对象中有空的观察者列表
   var dep = new Dep();
   //获取描述属性
   var property = Object.getOwnPropertyDescriptor(obj, key);
   var _property = Object.getOwnPropertyNames(obj); //获取实力对象属性或者方法,包括定义的描述属性
   console.log(property);
   console.log(_property);

   if (property && property.configurable === false) {
     return;
   }

   // cater for pre-defined getter/setters

   var getter = property && property.get;
   console.log("arguments.length=" + arguments.length);

   if (!getter && arguments.length === 2) {
     val = obj[key];
   }
   var setter = property && property.set;
   console.log(val);
   //判断value 是否有__ob__    实例化 dep对象,获取dep对象  为 value添加__ob__ 属性递归把val添加到观察者中  返回 new Observer 实例化的对象
   var childOb = !shallow && observe(val);
   //定义描述
   console.log('key==============',key)
   Object.defineProperty(obj, key, {
     enumerable: true,
     configurable: true,
     get: function reactiveGetter() {
       var value = getter ? getter.call(obj) : val;
       if (Dep.target) {
         //Dep.target 静态标志 标志了Dep添加了Watcher 实例化的对象   JS的浏览器单线程特性,保证这个全局变量在同一时间内,只会有同一个监听器使用
         //添加一个dep
         dep.depend();
         if (childOb) {
           //如果子节点存在也添加一个dep
           childOb.dep.depend();
           if (Array.isArray(value)) {
             //判断是否是数组 如果是数组
             dependArray(value); //则数组也添加dep
           }
         }
       }
       return value;
     },
     set: function reactiveSetter(newVal) {
       var value = getter ? getter.call(obj) : val;
       /* eslint-disable no-self-compare  新旧值比较 如果是一样则不执行了*/
       if (newVal === value || (newVal !== newVal && value !== value)) {
         return;
       }
       /* eslint-enable no-self-compare
        *   不是生产环境的情况下
        * */
       if ("development" !== "production" && customSetter) {
         customSetter();
       }
       if (setter) {
         //set 方法 设置新的值
         setter.call(obj, newVal);
       } else {
         //新的值直接给他
         val = newVal;
       }
       console.log(newVal);

       //observe 添加 观察者
       childOb = !shallow && observe(newVal);
       //更新数据
       dep.notify();
     },
   });
 }

好的我们就先不说这么远,不然文章就会太长了。我们改好代码之后来测试下。

   <div id="app">
      <button @click="change">改变索引数组</button>
      <button @click="add">增加数组</button>
      <button @click="remove">删除数组</button>
      <ul>
        <li  v-for="(item,index) in array">
          {{item}} -- {{index}}
        </li>
      </ul>
    </div>
    <script>
      var app = new Vue({
        el: "#app",
        methods: {
          change: function () {
            this.array[0] = {
              age: 18,
            };
          },
          add: function () {
            this.array.push({
              age: (this.array.length+1) * 10,
            });
          },
          remove() {
            this.array.splice(0, 1);
          },
        },
        data: function () {
          return {
            array: [
              {
                age: 10,
              },
              {
                age: 20,
              },
              {
                age: 30,
              },
            ],
          };
        },
      });
    </script>

这个时候我们点击下 改变索引数组 按钮确实是能监听到了视图数据变化。所以说vue是可以做到监听数组索引下数据变化的,那为什么没有监听不这么做。下面我们就来分析下吧。

1.因为数组是可变动的。数组在快速增删的时候还在枚举增删的时候其实会出现一个bug,或者说他的你增删的时间快过于他重排的时候索引值就会出现一些问题。 比如下面代码 我们在循环数组时候 快速删除他,这个时候你会发现会有 数组 有一些删除不干净

    let arr = [];
      for (let x = 0; x < 1000; x++) {
        arr.push(x);
      }

      for (let y = 0; y < arr.length - 1; y++) {
        let index = Math.floor(Math.random() * arr.length);
        arr.splice(index, 1);
      }

      console.log("arr==", arr);

Vue为何不能监听数组的变化 如  this.arr[1]='abc'

实际上面代码运行结果我的期望是 arr=[] 是等于一个空数组了 然而他并不是 ,数组还等于500,还有这个还跟电脑配置有关系,因为用户电脑不同而影响 数组删除后或者增加后重排速度也有所不同。

然而这个问题在vue中也存在。因为我在看vue源码的时候知道vue源码从模版到数据加入响应式的时候是经过大量枚举的,比如会先经过模版解析,paserHtml 去循环一个个html字符串 然后用正则去匹配 标签,变成ast,还有一些可执行的jsx函数, 这个jsx函数就是defineProperty get中的函数。然后vue中的data数据经过多次深层低估把数据添加到defineProperty中监听,这里用到了大量枚举,还有就是用户删除数组的时候,那个所以重排时间是不确定的,vue也不确定什么时候再次 清理依赖和重新加入依赖,并且这个问题很容易导致vue中的defineProperty中的索引key出现扰乱问题。这个问题我觉得vue作者 尤雨溪 估计很难处理,因为数组长度重排时间是不确定的。因为不同的浏览器和同的客户端机器导致时间不同。所以估计这个问题很难处理。 还有一个就是比如100的一个数组,删除了第一个 那剩余99个是需要重新清楚监听依赖之后,在加入监听依赖。这样需要重新递归99个数据性能非常损耗。所以出于这两个问题尤雨溪就直接不监听数组索引数据改变的功能了。

下面我们来测试下vue刚才把 数组索引数据改变 的功能打开看看出现哪些问题, 修改vue 源码

  var Observer = function Observer(value) {
  // var Observer = function Observer(value) {
    console.log('value=======',value)
    debugger
    this.value = value;
    this.dep = new Dep();
    this.vmCount = 0;
    //设置监听 value 必须是对象
    def(value, "__ob__", this);

    // 不管数组还是对象直接加入监听
    this.walk(value);
    // 注释掉
    // if (Array.isArray(value)) { //如果是数组 是需要转换 过滤掉数组索引
    //   //判断是不是数组
    //   var augment = hasProto //__proto__ 
    //     ?
    //     protoAugment :
    //     copyAugment;
    //   augment(value, arrayMethods, arrayKeys);
    //   this.observeArray(value); 
    // } else {
    //   this.walk(value);
    // }
  };

测试用例

    <div id="app">
      <button @click="change">改变索引数组</button>
      <button @click="add">增加数组</button>
      <button @click="remove">删除数组</button>
      <ul>
        <li v-for="(item,index) in array">{{item}} -- {{index}}</li>
      </ul>
    </div>
    <script>
      var app = new Vue({
        el: "#app",
        methods: {
          change: function () {
            this.array[0] = {
              age: 18,
            };
          },
          add: function () {
            this.array.push({
              age: (this.array.length + 1) * 10,
            });
          },
          remove() {
            this.array.splice(0, 1);
          },
        },
        data: function () {
          return {
            array: [
              {
                age: 10,
              },
              {
                age: 20,
              },
              {
                age: 30,
              },
            ],
          };
        },
      });
      
     
    </script>


我们点击删除数组 按钮 发现最后一个删除不掉

Vue为何不能监听数组的变化 如  this.arr[1]='abc'

然后点击添加数组 也发现他监听出了问题。

所以vue打开 数组索引监听数据 功能上是可以实现,但是会有bug。 基于bug和性能问题 尤雨溪把这个功能去掉了。

如果你对vue源码有兴趣可以看我的 vue源码逐行分析 www.cnblogs.com/hao123456/p…

文章 参考 github.com/vuejs/vue/i…