Vue为何不能监听数组的变化

1,542 阅读4分钟

前言

今天在面试的时候被问到,vue2有什么不足之处?我脑海中朦朦胧胧的看过几篇文章,说vue2监测不到对数组的变化。同时,在看一个项目的时候,记得通过arr[0] = 1修改数组,结果页面内容没有任何变化。于是不假思索的说到“vue2不能监测数组的变化”。面试官又问,嗯?为什么不能监测呢?你说说呢?这一点我还真没进行深入研究,就单纯只知道它不能监测数组的变化。

这就来仔细分析下为什么它对数组有特殊的处理。是因为它监测不了吗?还是别的原因?更或者说它可以监测到数组数据的变化方式,只是我回答错误了。

1、Vue2.0中监听数据据

众所周知啊,vue2.0中监听数据是通过Object.defineProperty递归查询对象全部属性,从而实现对一个对象全部属性的监听。我们首先来验证一下,Object.defineProperty这个API能否监测数组的变化。

function defineReactive(data, key, value) {
    Object.defineProperty(data, key, {
        enumerable: true,
        configurable: true,
        get() {
            console.log(`您试图访问value值: ${value}`)
            return value
        },
        set(newVal) {
            console.log(`您试图改变value值: ${newVal}`)
            value = newVal
        }
    })
}
        
function observe(data) {
    Object.keys(data).forEach(function(key) {
        defineReactive(data, key, data[key])
    })
}
 
let arr = ["apple", "banana"]
observe(arr)

通过以上代码,我们可以得出结论,通过Object.defineProperty这个API是可以监测到数组的变化的。

image.png

那么问题来了,既然vue2是通过Object.defineProperty这个API来监测数据的,那么为什么数组就不行呢?还是说它vue对数组有特殊的处理?

2、验证vue2中改变数组

我们简单通过几行代码回顾一下vue2对数组变化的监测。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script src="./vue2.js"></script>
</head>
<body>
    <div id="app">
        <h1>{{arr}}</h1>
        <button @click="changeval">change</button>
        <button @click="changeval2">change2</button>
        <button @click="printarr">print</button>
    </div>
    <script>
        var v = new Vue({
            el:"#app",
            data(){
                return{
                    arr : ["apple", "banana"]
                }
            },
            methods:{
                changeval(){
                    this.arr[0] = 10
                },
                changeval2(){
                    this.arr.splice(0, 1, 10)
                },
                printarr(){
                    console.log(this.arr);
                }
            }
        })
    </script>
</body>
</html>
  • 首先我定义了一个changeval方法,此方法是通过直接赋值的方式this.arr[0] = 10这种方式改变数组。同时定义了一个printarr方法在控制台输出数组。让我们来看一下vue2能否检测到数组的变化(直接赋值)

vuearr2.gif

当我们点击change按钮时,arr的确被改变了,但是vue没有捕获到。

  • 当我们通过第二种方式-splice方法,改变数组,看vue2能否监测到呢?

vuearr.gif

通过上述实验可以得出,通过splice这种方式改变数组,vue2就可以监测到

我们可以得出一个大概的结论,就是vue2是通过数组的一些操作(如splice等)来监测数组的变化的。

3、vue2监测数组变化原理

通过阅读源码,我们发现,在用Object.defineProperty监测数据的时候,它对Array做了特殊处理。不使用下面代码对数据进行监听

var keys = Object.keys(value);
  for (var i = 0; i < keys.length; i++) {
      var key = keys[i];
      defineReactive(value, key, NO_INIITIAL_VALUE, undefined, shallow, mock);
  }

image.png

我们尝试修改一下,把它改成对数组也进行defineReactive

image.png

用之前同样的代码,我们这次通过changeval(this.arr[0] = 10)来修改数组,看vue2是否能够监测到。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script src="./vue2.js"></script>
</head>
<body>
    <div id="app">
        <h1>{{arr}}</h1>
        <button @click="changeval">change</button>
        <button @click="changeval2">change2</button>
        <button @click="printarr">print</button>
    </div>
    <script>
        var v = new Vue({
            el:"#app",
            data(){
                return{
                    arr : ["apple", "banana"]
                }
            },
            methods:{
                changeval(){
                    this.arr[0] = 10
                },
                changeval2(){
                    this.arr.splice(0, 1, 10)
                },
                printarr(){
                    console.log(this.arr);
                }
            }
        })
    </script>
</body>
</html>

可以发现,数组中数据变化被vue2检测到。

vuearr3.gif


可见,使用Object.defineProperty是可以监测到数组中值的变化的,那为什么vue2没有这样做呢?那为什么通过splice等方式改变数组值就可以被vue2检测到呢

4、数组数据是怎么被监听的

首先来回答第二个问题,为什么通过splice等方式改变数组值就可以被vue2检测到呢?原因是vue2对数组的方法进行重写

var arrayProto = Array.prototype;
var arrayMethods = Object.create(arrayProto);
var methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
];
/**
* Intercept mutating methods and emit events
*/
methodsToPatch.forEach(function (method) {
  // cache original method
  var original = arrayProto[method];
  def(arrayMethods, method, function mutator() {
      var args = [];
      for (var _i = 0; _i < arguments.length; _i++) {
          args[_i] = arguments[_i];
      }
      var result = original.apply(this, args);
      var ob = this.__ob__;
      var 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({
              type: "array mutation" /* TriggerOpTypes.ARRAY_MUTATION */,
              target: this,
              key: method
          });
      }
      return result;
  });
});

可以看到,vue2对'push','pop','shift','unshift','splice','sort','reverse'这些方法进行了重写

5、Vue为什么不通过Object.defineProperty检测数组变动

性能问题

请参考尤大的回答:为什么vue没有提供对数组属性的监听

个人认为:在js中,数组经常被当作栈、队列,大多数情况,我们对数组的操作就是遍历,如果用Object.defineProperty监测它的变化,这个就会有很大的性能问题。正如尤大的“性能代价和获得的用户体验收益不成正比。
所以 Vue 不在数组每个键上设置,而是在数组上定义 __ob__ (var ob = this.__ob__;) ,并且替换了 push 等等能够影响原数组的原型方法。

总结

所以,vue2并不是不能监测数组的变化,而是不能监听直接给数组赋值的这种变化(this.arr[0] = 10)。因为性能问题,vue2并没有使用Object.defineProperty来监测数组,而是通过重写数组方法的这一方式,来监测数组变化。