监听数组变化,实现响应

举个例子,来说明下为什么监听不到数组变化

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覆盖了,便有了现在的结果。

posted @ 2020-02-13 00:05  灰姑娘的冰眸  阅读(1316)  评论(0编辑  收藏  举报