监听数组变化,实现响应
举个例子,来说明下为什么监听不到数组变化
var target ={ val: 1 } let _value = target.val Object.defineProperty(target,"val",{ get:function(){ return _value }, set:function(newVal){ _value = newVal console.log("setted") } }) console.log(target.val) // 1 console.log(target.val = []) // setted [] console.log(target.val = [1,2,3]) // setted [1,2,3] console.log(target.val[1]=10) // 10 console.log(target.val.push(8)) // 4 console.log(target.val.length=5) // 5 console.log(target.val) // [1, 10, 3, 8, empty]
从本例中可以看到,当taget.val被设置为数组后,想要对数组内部进行修改,通过数组索引去赋值 target.val[1]=10 ,不会触发set方法执行。
那么该如何实现呢?
我们先来了解下 Array.prototype.push.call() 相关知识,便于监听数组,实现响应做铺垫。
Array.prototype.push.apply()
var a = [1,2,3]; var b = [4,5,6]; Array.prototype.push.apply(a, b); console.log(a) //[1,2,3,4,5,6]
原生push方法接受的参数是一个参数列表,它不会自动把数组扩展成参数列表,使用apply的写法可以将数组型参数扩展成参数列表,这样合并两个数组就可以直接传数组参数了。
注:合并数组为什么不直接使用Array.prototype.concat()呢?
因为concat不会改变原数组,concat会返回新数组,而上面apply这种写法直接改变数组a。
简单实现监听数组变化
let arr = [1,2,3]
console.log(arr)
打印结果看来,数组的隐式原型上挂载了一些方法,如push()、pop()、shift()、unshift()、splice()、sort()、reverse()等。
我们重新改写下方法
let arr = [1,2,3] arr.__proto__ = { push: function() { // 这里的this指arr,即[1,2,3] return Array.prototype.push.apply(this, arguments) } } arr.push(6) console.log('修改后数组:',arr)
在官方文档,所需监视的只有 push()、pop()、shift()、unshift()、splice()、sort()、reverse() 7 种方法。我们可以遍历一下:
只需要监听我们需要监听的数据数组的一个变更,而不是针对原生Array的一个重新封装。
会重写Array.prototype.push方法,并生成一个新的数组赋值给数据,这样数据双向绑定就会触发。
首先让这个对象继承 Array
本身的所有属性,这样就不会影响到数组本身其他属性的使用,后面对相应的函数进行改写,也就是在原方法调用后去通知其它相关依赖这个属性发生了变化,这点和 Object.defineProperty
中 setter
所做的事情几乎完全一样,唯一的区别是可以细化到用户到底做的是哪一种操作,以及数组的长度是否变化
不会污染到原生Array上的原型方法。
首先我们将需要监听的数组的原型指针指向newArrProto,然后它会执行原生Array中对应的原型方法,与此同时执行我们自己重新封装的方法。
那么问题来了,这种形式咋这么眼熟呢?这不就是我们见到的最多的继承问题么?子类(newArrProto)和父类(Array)做的事情相似,却又和父类做的事情不同。但是直接修改__proto__隐式原型指向总感觉心里怪怪的(因为我们可能看到的多的还是prototype),心里不(W)舒(T)服(F)。
const arrayProto = Array.prototype; const arrayMethods = Object.create(arrayProto); const newArrProto = []; ['push', 'pop','shift','unshift','splice','sort','reverse'].forEach(method => { // 原生Array的原型方法 let original = arrayMethods[method]; // 将push,pop等方法重新封装并定义在对象newArrProto的属性上 // 注:封装好的方法是定义在newArrProto的属性上而不是其原型属性,即newArrProto.__proto__ 没有改变 newArrProto[method] = function mutator() { console.log('监听到数组的变化啦!');
// 更新视图,dep.notify() // 调用对应的原生方法并返回结果(新数组长度) return original.apply(this, arguments); } }) let list1 = [1, 2]; // 将我们要监听的数组的原型指针指向上面定义的空数组对象 // newArrProto的属性上定义了我们封装好的push,pop等方法 list1.__proto__ = newArrProto; list1.push(3); // 监听到数组的变化啦! 3 // list2没有被重新定义原型指针,所以这里会正常执行原生Array上的原型方法 let list2 = [1, 2]; list2.push(3); // 3
Array.prototype.push.call()
var obj = {} console.log(Array.prototype.push.call(obj, 'a','b','c')) // 3 console.log(obj) // {0: "a", 1: "b", 2: "c", length: 3} var obj1 = { length: 5 } console.log(Array.prototype.push.call(obj1, 'a','b','c')) // 8 console.log(obj1) // {5: "a", 6: "b", 7: "c", length: 8} var obj2 = { 0: 'e', 1: 'f', length: 7 } console.log(Array.prototype.push.call(obj2, 'a','b','c')) // 10 console.log(obj2) // {0: "e", 1: "f", 7: "a", 8: "b", 9: "c", length: 10}
通过上面对比结果,我们可以看出:
1)当对象中不含有length属性时,调用数组原型方法push,将对象转为类数组对象,新增属性的索引从0开始,且lengt指是新增属性的个数
2)当对象中含有length属性时,新增属性的索引命名从length长度开始计算。
eg: obj1中length为5,新增加属性的索引分别为5、6、7;obj2中length为7,新增加属性的索引分别为7、8、9
Array.prototype.slice.call()
Array.prototype.slice.call()方法是只能在类数组上起作用的,并不能同push()方法一样可以可以使对象转换为带有length属性的类数组对象。
结论,当对象中没有length属性时,默认添加的新属性索引应为0,因为a中已经有为0的key了,于是将原来的banana覆盖了,便有了现在的结果。