为什么defineProperty不能检测到数组长度的变化

正如Vue文档中所述:

由于 JavaScript 的限制,Vue 无法检测到以下数组变动:1. 当你使用索引直接设置一项时,例如 vm.items[indexOfItem] = newValue 2. 当你修改数组长度时,例如 vm.items.length = newLength

首先,明白vue对于监听数据的变动是通过ES5的Object.defineProperty()来实现的,聊聊Vue的双向数据绑定 中已经说过Vue对于监测数组的变动是重写的数组的原型来达到目的的,原因是defineProperty不能检测数组的长度变化,准确来说是 通过改变数组的length而增加的长度是不能被监测到的

  1. Object.defineProperty()

Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。

1
2
3
4
5
6
Object.defineProperty(obj, "a", {
value : 1,
writable : false,
configurable : false,
enumerable : false
});
  • [[Writable]] 是否可写

  • [[Configurable]] 字面理解是表示属性是否可配置——能否修改属性;能否通过delete删除属性;能否把属性修改为访问器属性

  • [[Enumerable]] 能否通过for或for-in循环返回该属性

  1. 关于数组length属性的特性

数组的length属性被初始化为:

1
2
3
writable	true
enumerable false
configurable false

看到configurable为false也就是说,我们想要通过get/set方法来监听length属性是不可行的。

1
2
3
4
5
6
var arr = [1, 2, 3];
arr.length = 5;

for(var key in arr) {
console.log(key); // 0, 1, 2
}

可以看到并没有打印出来新增加长度部分的键,因为length属性可以显式地去赋值的,但是新增加的长度(3和4)对应的键值是undefined,但是索引3和4的key是没有值的,上面的打印结果也可以说明defineProperty没办法对未知属性进行监听。

  1. 验证一下数组的几个内置的方法对索引的影响:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    function defineReactive(data, key, val) {
    Object.defineProperty(data, key, {
    enumerable: true,
    configurable: true,
    get: function defineGet() {
    console.log(`get key: ${key} val: ${val}`);
    return val;
    },
    set: function defineSet(newVal) {
    console.log(`set key: ${key} val: ${newVal}`);
    val = newVal;
    }
    })
    }

    function observe(data) {
    Object.keys(data).forEach(function(key) {
    defineReactive(data, key, data[key]);
    })
    }

    let test = [1, 2, 3];
    // 初始化
    observe(test);
    console.log(observe(test)); // 遍历数组索引并打印对应的值
    // get key: 0 val: 1
    // get key: 1 val: 2
    // get key: 2 val: 3

    test.push(4); // 新增了索引,但是可以看到新的索引并没有被observe
    console.log(observe(test));
    // get key: 0 val: 1
    // get key: 1 val: 2
    // get key: 2 val: 3

    test[3] = 5;
    console.log(observe(test)); // 修改新增的索引对应的值触发了对应的set方法,set了的新索引的值被observe时触发了get
    // set key: 3 val: 5
    // get key: 0 val: 1
    // get key: 1 val: 2
    // get key: 2 val: 3
    // get key: 3 val: 5

    test.pop(); // 弹出新的索引对应的值时触发了get方法
    console.log(observe(test));
    // get key: 3 val: 5
    // get key: 0 val: 1
    // get key: 1 val: 2
    // get key: 2 val: 3

    test[3] = 10; // 将刚刚弹出的索引重新赋值发现并没有触发set方法,所以已经删除的索引不会observe
    console.log(observe(test));
    // get key: 0 val: 1
    // get key: 1 val: 2
    // get key: 2 val: 3

    test.unshift(6); // 给数组的第一个位置添加新元素时,首先遍历除首元素外的所有索引和值并存放,然后重新对各索引赋值
    console.log(observe(test));
    // get key: 2 val: 3
    // get key: 1 val: 2
    // set key: 2 val: 2
    // get key: 0 val: 1
    // set key: 1 val: 1
    // set key: 0 val: 6
    // get key: 0 val: 6
    // get key: 1 val: 1
    // get key: 2 val: 2

    test.length = 20; // 可以看到数组的长度虽然更新了,但是新增的索引都是空并不会遍历数组去赋值索引,但是新增索引的值都为undefined
    console.log(test);
    // [ [Getter/Setter],
    // [Getter/Setter],
    // [Getter/Setter],
    // [Getter/Setter],
    // [Getter/Setter],
    // <15 empty items> ]
    console.log(observe(test));
    // get key: 0 val: 6
    // get key: 1 val: 1
    // get key: 2 val: 2
    // get key: 3 val: 3
    // get key: 4 val: 10

所以,Vue在文档中声明只能通过提供一些变异方法来对数组进行更新。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// 源码目录:src/core/observer/array.js
const arrayProto = Array.prototype // 获取原生数组的原型
// 创建一个新对象,修改该对象上的数组的七个方法,防止污染原生数组方法
// 数组构造函数的原型也是个数组,数组并没有索引,因为length = 0,
// 相反的拥有属性,属性名为数组方法,值为对应的函数
export const arrayMethods = Object.create(arrayProto)

// 重写数组的以下方法,截获数组的成员发生的变化,执行原生数组操作的同时dep通知关联的所有观察者进行响应式处理
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]

methodsToPatch.forEach(function (method) {
// 缓存原生数组
const original = arrayProto[method]
// def使用Object.defineProperty重新定义属性
def(arrayMethods, method, function mutator (...args) {
const result = original.apply(this, args) // 调用原生数组的方法

const ob = this.__ob__ // ob就是observe实例observe才能响应式
let inserted
switch (method) {
// push和unshift方法会增加数组的索引,但是新增的索引位需要手动observe的
case 'push':
case 'unshift':
inserted = args
break
// 同理,splice的第三个参数,为新增的值,也需要手动observe
case 'splice':
inserted = args.slice(2)
break
}
// 其余的方法都是在原有的索引上更新,初始化的时候已经observe过了
if (inserted) ob.observeArray(inserted)
// dep通知所有的订阅者触发回调
ob.dep.notify()
return result
})
})