之前学习了关于Vue
的响应式数据的原理: Vue 的双向绑定原理及手把手实现。它的原理其实就是通过Object.defineProperty
控制 getter
和setter
结合发布订阅者模式完成响应式设计的,
1. 但是这种数据劫持对数组有什么影响呢?
这种递归方式无论对于数组还是对象都进行了观测。但是我们的数组有成千上万个元素,每一个元素下标都添加get
和set
.这样对于性能来说代价太大了。那么Object.property
只用来劫持对象。
2. Object.property这种劫持方式有什么缺点呢?
对于新增的或者删除的属性是无法被检测到的,只有对象本身存在的属性才会被劫持。
对于数组来说也是一样,新增加的元素和删除的元素无法对他们的下表进行劫持。
根据以上两点可以得出这就是为什么Vue
官方说如下的话:
由于JavaScript
的限制,Vue
无法检测到以下数组的变动:
- 当你使用索引设置一项时;如:
vm.items[indexOfItem] = newValue
- 当修改数组的长度时;如:vm.items.length = newLength
我们举个例子:
之前的文章 Vue 的双向绑定原理及手把手实现写好了Observer
。 我们来做一个实验:
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
| <html> <header></header> <body> <div id="app"></div></body> <script src="./js/observer.js"></script> <script src="./js/index.js"></script> <script src="./js/compile.js"></script> <script src="./js/watcher.js"></script> <script> const vm = new MyVue({ el: "#app", data: { name: "舒丽琦", list: ["1", "2", "3"] } }) vm.name = " 小小舒" vm.name;
vm.list.push("4"); vm.list[0]; console.log(vm.list); </script> </html>
|
关上面的代码的代码可到 Vue 的双向绑定原理及手把手实现找到
我们看看实验的结果:
从结果可以看出:data
中的 name
, list
均发生了变化; name
发生了变化能检测到,但是是list
发生变化无法检测到。这是为什么呢?
原来操作数组的方法是在 Array.prototype
上的; 挂在在Array.prototype
上的方法并不能触发属性的getter
和setter
。
Vue 检测数组变化-重写数组
那解决这个问题的办法是什么呢?Vue2.x
使用的是将数组常用的方法进行重写。
基本的思路是之前我们调用数组的常用的方法的时候(如push
·);我们是从 Array.prototype
上面寻找这个方法,现在我们改成一个空对象 {} 继承 Array.prototype
,然后给 空对象添加 push
方法;
1 2 3 4
| { push: function() {} }
|
这样,我们调用常用的办法,实际上就是在调用 这个空对象的方法。因为常用的方法使我们自己重写的,肯定就知道当前操作是什么,新数据是什么等等,就可以做到检测数组的变化了。
既然是这样, 那么我们的observer
是需要区分当前需要监听的数据是对象还是数组,如果是数组,则改变数组的原型链,只有改变了原型链才能改变调用数组常用的方法如push
方法时,是调用我们自己的设置的常用的方法的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| function Observer(data) { const _this = this; if (!data || typeof data !== "object") { return; } if (Array.isArray(data)) { data.__proto__ = arrayMethods; } else { Object.keys(data).forEach(function (key) { defineReactive(data, key, data[key]); }) } }
|
上面代码的arrayMethods
就是我们所说的空对象。它里面添加数组常用的方法如:push
等
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
const oldArrayProperty = Array.prototype;
const arrayMethodObj = Object.create(oldArrayProperty);
const arrayMethods = ["push", "shift", "unshift", "pop","reverse", "sort", "splice"]; arrayMethods.forEach(method => { arrayMethodObj[method] = function(...arg) { const result = oldArrayProperty[method].apply(this, arg); console.log(`数组有变化了,方法:${method}, 新增加的值为: ${inserted}`); return result; } })
|
下面我们实现对新增属性的监听。基本的思路:
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
|
const oldArrayProperty = Array.prototype;
const arrayMethodObj = Object.create(oldArrayProperty);
const arrayMethods = ["push", "shift", "unshift", "pop","reverse", "sort", "splice"]; arrayMethods.forEach(method => { arrayMethodObj[method] = function(...arg) { const result = oldArrayProperty[method].apply(this, arg); let inserted; switch(method) { case "push": case "unshift": inserted = arg; break case "splice": inserted = arg.slice(2); break default: break; } if (inserted) { observerArray(inserted); } console.log(`数组有变化了,方法:${method}, 新增加的值为: ${inserted}`); return result; } })
|
上面代码中有一个 observeArray
方法去监听新增加的数组的元素。我们看看 observeArray
方法。
1 2 3 4 5 6 7
| function observeArray(items) { for (var i = 0, l = items.length; i < l; i++) { observer(items[i]); } }
|
observeArray
方法中对inserted
进行遍历,对每一项进行监听。为什么要遍历呢?因为inserted
不一定是一个值。也有可能是多个如:[].splice(0,0,”1”, “2”, “3”);[].push(1,2,3)等。
我们来看 observer
方法:
1 2 3 4 5 6 7
| function observer(value) { if (!value || typeof value !== "object") { return; } return new Observer(value); }
|
这里有很重要的一点:value
不是一个对象的话, 我们是不做任何处理的。就比如: observer(items[i]); // 注意这里是observer 不是 Observer
这一句,这里的items[i]
有可能就只是一个数据,而不是对象或者数组, 我们就是不处理的。不然跟对数组的所有下表监听的有啥区别。
目前实现对了数组方法的拦截。但是还有一个问题,就是我们在初始化的时候,data可能就是数组,因此要把这个数组也进行监听。
1 2 3 4 5 6 7 8 9 10
| function Observer(data) { if (Array.isArray(data)) { data.__proto__ = arrayMethodObj; observerArray(data); } else { Object.keys(data).forEach((key) => { defineObserver(data, key, data[key]); }) } }
|
最后我们是实现了对数据的监听,不过这里还是有个问题没解决,也就是Vue2.x
还没有解决的问题:并没有实现对数组的每一项进行监听:如下面这样的就不会被监听到
1 2 3 4 5 6 7 8
| const vm = new MyVue({ el: "#app", data: { name: "舒丽琦", list: ["1", "2", "3"] } }) vm.list[0] = "我改变了"
|
这是因为我们数据劫持的时候没有对数组的下标进行监听。因为性能的代价太高了。除此之外,改变数组的长度也是无限监听的 vm.list.length = 9
。
结果
最后我们演示一下结果
我们的html
代码为:
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
| <html> <header></header> <body> <div id="app"></div> </body> <script src="./js/observer.js"></script> <script src="./js/index.js"></script> <script src="./js/compile.js"></script> <script src="./js/watcher.js"></script> <script> const vm = new MyVue({ el: "#app", data: { name: "舒丽琦", list: ["舒", "丽", "琦"] } }) vm.list.push(["小小舒", "sha"]);
vm.list[3].push("哈哈哈")
vm.list.splice(3, 0,"哈哈哈", "怎么着");
console.log(vm.list); </script> </html>
|
observer.js
完整的代码:
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 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105
|
const oldArrayProperty = Array.prototype;
const arrayMethodObj = Object.create(oldArrayProperty);
const arrayMethods = ["push", "shift", "unshift", "pop","reverse", "sort", "splice"]; arrayMethods.forEach(method => { arrayMethodObj[method] = function(...arg) { const result = oldArrayProperty[method].apply(this, arg); let inserted; switch(method) { case "push": case "unshift": inserted = arg; break case "splice": inserted = arg.slice(2); break default: break; } if (inserted) { observerArray(inserted); } console.log(`数组有变化了,方法:${method}, 新增加的值为: ${inserted}`); return result; } })
function Observer(data) { if (Array.isArray(data)) { data.__proto__ = arrayMethodObj; observerArray(data); } else { Object.keys(data).forEach((key) => { defineObserver(data, key, data[key]); }) } } function observerArray(items) { for (let i = 0; i < items.length; i++) { observer(items[i]); } }
function observer(value) { if (!value || typeof value !== "object") { return; } return new Observer(value); }
function defineObserver(data, key, value) { observer(value); const dep = new Dep(); Object.defineProperty(data, key, { get: function() { if (Dep.target) { dep.addSub(Dep.target) } return value; }, set: function(newValue) { if (value !== newValue) { console.log("监听对象属性到变化了,新的值为:", newValue) value = newValue; dep.notify(); } } }) }
function Dep() { this.subs = []; } Dep.prototype = { addSub: function(sub) { this.subs.push(sub); }, notify: function() { this.subs.forEach((sub) => { sub.update(); }) } }; Dep.target = null;
|
结果:
上面例子的代码: vue检测数组-重写数组常用的方法