欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

vue中遇到的坑之变化检测问题(数组相关)

程序员文章站 2022-03-07 17:58:31
最近在项目中遇到了一个问题,不知道为什么,所以最后通过动手做demo实践、查文档的方式解决了,这里做一个总结。 例1 ...

最近在项目中遇到了一个问题,不知道为什么,所以最后通过动手做demo实践、查文档的方式解决了,这里做一个总结。

例1

<!doctype html>
<html lang="en">
<head>
 <meta charset="utf-8">
 <title>vue</title>
 <script src="https://unpkg.com/vue@2.3.3/dist/vue.js"></script>
 <style>
  li:hover {
   cursor: pointer;
  }
 </style>
</head>
<body>
 <div class="wrap">
  <ul>
   <li v-for="item,index in items" v-on:click="handle(index)">
    <span>{{item.name}}</span>
    <span>{{numbers[index]}}</span>
   </li>
  </ul>
 </div>
 <script>
  var vm = new vue({
   el: ".wrap",
   data: {
    numbers: [],
    items: [
     {name: 'jjj'},
     {name: 'kkk'},
     {name: 'lll'},
    ]
   },
   methods: {
    handle: function (index) {
     // why: 更新数据,view层未渲染,但通过console这个数组可以发现数据确实更新了
      if (typeof(this.numbers[index]) === "undefined" ) {
       this.numbers[index] = 1;
      } else {
       this.numbers[index]++;
      }
    }
   }
  });
 </script>
</body>
</html>

这里的实现目的很明确 --- 我希望在点击li时先检测是否存在,当然是不存在的,所以就将值设置为1, 如果再次点击,就让数字累加。

但是出现的问题是: 点击之后数字并没有在view层更新,而通过console打印发现数据确实更新了,只是view层没有及时的检测到, 而我一直以来的想法就是: 既然vue实现的时数据双向绑定,那么在model层发生了变化之后为什么就没有在view层更新呢? 

首先,我就考虑了这是不是数组的问题,于是,我测试了下面的例子:

例2

<!doctype html>
<html lang="en">
<head>
 <meta charset="utf-8">
 <title>vue</title>
 <script src="https://unpkg.com/vue@2.3.3/dist/vue.js"></script>
 <style>
  li:hover {
   cursor: pointer;
  }
 </style>
</head>
<body>
 <div class="wrap">
  <ul>
   <li v-for="item,index in items" v-on:click="handle(index)">
    <span>{{item.name}}</span>
    <span>{{numbers[index]}}</span>
   </li>
  </ul>
 </div>
 <script>
  var vm = new vue({
   el: ".wrap",
   data: {
    numbers: [],
    items: [
     {name: 'jjj'},
     {name: 'kkk'},
     {name: 'lll'},
    ]
   },
   methods: {
    handle: function (index) {
     // 不是数组,这里更新数据就可以直接在view层渲染
     this.items[index].name += " success";
    }
   }
  });
 </script>
</body>
</html>

这时,我再测试时就发现,这里的model层发生了变化时,view层就能及时、有效的得到更新。

而数组为什么不可以呢? 

于是在文档上的一个不起眼的地方找到了下面的说明:

vue中遇到的坑之变化检测问题(数组相关)

其中最重要的一句话就是 --- 如果对象是响应式的,确保属性被创建后也是响应式的,同时触发视图更新,这个方法主要用于避开vue不能检测到属性被添加的限制。

那么什么情况下vue是不能检测到属性被添加呢?  根据参考链接,我们在文档上看到了很好的说明 ---

首先,我们要了解vue是如何实现数据的双向绑定的! 

把一个普通 javascript 对象传给 vue 实例的 data 选项,vue 将遍历此对象所有的属性,并使用 object.defineproperty 把这些属性全部转为 getter/setter。object.defineproperty 是仅 es5 支持,且无法 shim 的特性,这也就是为什么 vue 不支持 ie8 以及更低版本浏览器的原因。

知识补充:

访问器属性不包含数据值,他们包含一对getter函数和setter函数(这两个函数不是必须的)。在读取访问器属性时,会调用getter函数,这个函数负责返回有效的值;在写入访问器属性是,会调用setter函数并传入新值,这个函数负责决定如何处理数据。

访问器属性不能直接定义,必须是用object.defineproperty()来定义。

下面是一个例子:

<!doctype html>
<html lang="en">
<head>
 <meta charset="utf-8">
 <title>vue</title>
</head>
<body>
 <script>
  var book={
    _year:2004,
    edition:1
  };
  object.defineproperty(book,"year",{
    get:function(){
      return this._year;
    },
    set:function(newvalue){
      if(newvalue>2004){
        this._year=newvalue;
        this.edition+=newvalue-2004;
      }
    }
  });
  console.log(book.year); // 2004 在读取访问器属性时会调用get函数
  book.year=2005; // 在给访问器属性赋值时会调用set函数
  console.log(book.edition); // 2
 </script>
</body>
</html>

这个例子应该可以很好的理解访问器属性了。

所以,当对象下的访问器属性值发生了改变之后(vue会将属性都转化为访问器属性,之前提到了), 那么就会调用set函数,这时vue就可以通过这个set函数来追踪变化,调用相关函数来实现view视图的更新。

每个组件实例都有相应的 watcher 实例对象,它会在组件渲染的过程中把属性记录为依赖,之后当依赖项的 setter 被调用时,会通知 watcher 重新计算,从而致使它关联的组件得以更新。

vue中遇到的坑之变化检测问题(数组相关)

即在渲染的过程中就会调用对象属性的getter函数,然后getter函数通知wather对象将之声明为依赖,依赖之后,如果对象属性发生了变化,那么就会调用settter函数来通知watcher,watcher就会在重新渲染组件,以此来完成更新。

ok!既然知道了原理,我们就可以进一步了解为什么出现了之前数组的问题了!

变化检测问题

收到现代javascript浏览器的限制,其实主要是 object.observe() 方法支持的不好,vue不能检测到对象的添加或者删除。然而vue在初始化实例时就对属性执行了setter/getter转化过程,所以属性必须开始就在对象上,这样才能让vue转化它。

所以对于前面的例子就不能理解了 --- 数组中index都可以看做是属性,当我们添加属性并赋值时,vue并不能检测到对象中属性的添加或者删除,但是其的确是添加或删除了,故我们可以通过console看到变化,所以就没有办法做到响应式; 而在第二个例子中,我们是在已有的属性的基础上进行修改的,这些属性是在最开始就被vue初始化实例时执行了setter/getter的转化过程,所以说他们的修改是有效的,model的数据可以实时的在view层中得到相应。

补充知识: 什么是 object.observe() ?

在介绍之前,不得不残忍的说,尽管这个方法可以在某些浏览器上运行,但事实是这个方法已经废弃!

概述: 此方法用于异步地监视一个对象的修改。当对象的属性被修改时,方法的回调函数会提供一个有序的修改流,然而这个接口已经从各大浏览器移除,可以使用通用的proxy 对象。      

方法:

object.observe(obj, callback[, acceptlist])

其中obj就是被监控的对象, callback是一个回调函数,其中的参数包括changes和acceptlist,

changes一个数组,其中包含的每一个对象代表一个修改行为。每个修改行为的对象包含:

  • name: 被修改的属性名称。
  • object: 修改后该对象的值。
  • type: 表示对该对象做了何种类型的修改,可能的值为"add", "update", or "delete"。
  • oldvalue: 对象修改前的值。该值只在"update"与"delete"有效。

acceptlist在给定对象上给定回调中要监视的变化类型列表。如果省略, ["add", "update", "delete", "reconfigure", "setprototype", "preventextensions"] 将会被使用。

var obj = {
 foo: 0,
 bar: 1
};

object.observe(obj, function(changes) {
 console.log(changes);
});

obj.baz = 2;
// [{name: 'baz', object: <obj>, type: 'add'}]

obj.foo = 'hello';
// [{name: 'foo', object: <obj>, type: 'update', oldvalue: 0}]

delete obj.baz;
// [{name: 'baz', object: <obj>, type: 'delete', oldvalue: 2}]

如上所示: 但是chrome也是不支持的,浏览器的兼容性如下:

vue中遇到的坑之变化检测问题(数组相关)

参考文档: object.ovserve()

推荐阅读文章: object.observe() 引爆数据绑定革命

解决方法

使用 vue.set(object, key, value) 方法将响应属性添加到嵌套的对象上。 还可以使用 vm.$set 实例方法,这也是全局 vue.set 方法的别名。

例3

<!doctype html>
<html lang="en">
<head>
 <meta charset="utf-8">
 <title>vue</title>
 <script src="https://unpkg.com/vue@2.3.3/dist/vue.js"></script>
 <style>
  li:hover {
   cursor: pointer;
  }
 </style>
</head>
<body>
 <div class="wrap">
  <ul>
   <li v-for="item,index in items" v-on:click="handle(index)">
    <span>{{item.name}}</span>
    <span>{{numbers[index]}}</span>
   </li>
  </ul>
 </div>
 <script>
  var vm = new vue({
   el: ".wrap",
   data: {
    numbers: [],
    items: [
     {name: 'jjj'},
     {name: 'kkk'},
     {name: 'lll'},
    ]
   },
   methods: {
    handle: function (index) {
     // why: 更新数据,view层未渲染,但通过console这个数组可以发现数据确实更新了
      if (typeof(this.numbers[index]) === "undefined" ) {
       this.$set(this.numbers, index, 1);
      } else {
       this.$set(this.numbers, index, ++this.numbers[index]);
      }
    }
   }
  });
 </script>
</body>
</html>

这样,我们就可以实现最终的目的了!

第二部分

上面一部分是指在data下的数组,而如果是在store中的数组,一般可以这样:

[add_one] (state, index) {
   if ( typeof state.numbers[index] == "undefined") {
    vue.set(state.numbers, index, 1)
   } else {
    vue.set(state.numbers, index, ++state.numbers[index])
   }
  }

即使用 vue.set() 的方式来改变、增加。

注意:这里是确定index的增加和减少,所以用 vue.set() 的方式

减的方式如下:

 [remove_one] (state, index) {
   vue.set(state.numbers, index, --state.numbers[index]);
  }

第四部分

如果是在store的actions中我们需要对stroe中的数组进行填充,方法如下:

state内容:

kindnames: []

mutations内容:

  [add_kind_name] (state, name) {
   state.kindnames.push(name);
  } 

注意: 这里直接使用push的方式

当然,除了push,我们还可以shift等各种方式。 

actions的内容:

commit(add_kind_name, state.items[index++].name);

这里,state.items[index++].name获取到的是一个一个的字符串。

最后,看到数据如下:

vue中遇到的坑之变化检测问题(数组相关)

注:同样可以参考文档 ---

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。