Skip to content

数组处理 ​

本节目标:理解“数组处理”的核心思路,并能把它用于实际开发或面试表达。

说明:原 OneNote 中有图片引用,但图片未包含在导出目录中;本页保留了可用的文字与代码内容。

注意:
Object.keys、Ojbect.values、Ojbect.entries都可以遍历数组;
此外,Object.defineProperty方法可以对数组的访问进行拦截,如下代码:
var a = [1, 2, 3, 4]
Object.entries(a).forEach(([k, v]) => {
    Object.defineProperty(a, k, {
        get() {
            // 闭包,此vaule会向上层的value进行查找
            return v + 1;
        },
        set(newValue) {
            v = newValue;
        }
    })
})
console.log(a[1]);// 输出为3
所以,之前的代码会对数组的每一项进行劫持;这不合适;

因为我们很少使用属性名,即键名的方式去对数组进行取值或赋值操作;

此时还对数据按索引进行截持时,如果数组长度大时,将会非常耗性能;

所以这种当成对象处理的方式不合适;
所以修改数组索引和修改数组长度不会引发更新;
但vue3中为了兼容proxy,内部对数组就是defineProperty

所以数组需要特殊处理: 正常用户修改数组,无非采用数组的变异方法, push、pop、splice、shift、unsfhit、reverse、sort

修改observe/index.js中逻辑
当new Observe(value)传入的值是数组时,我们进行特殊处理;
处理的方式是对数组的变异方法进行重写;
  • 然后再调用observeArray方法遍历数组,对数组每一项调用observe进行数据劫持,这样的话:

    如果数组成员是基本数据类型,不进行劫持。
    如果是objcet类型时,再进行劫持;
    如果是数组,我们递归进行处理;

这样的话,对于数组中的成员,如果是数组,它的原型上的七个变异方法都是被重写过的;

class Observer {
  constructor(value) {
    // 核心是循环对象
    ==if== ==(====isArray====(====value====)) {==
      ==//== ==更改数组原型方法====,== ==如果是数组== ==我就改写数组的原型链==
      ==value====.====__proto__== ===== ==arrayMethods====;== ==//== ==重写数组的方法==
      ==this====.====observeArray====(====value====);==
    ==}== ==else== =={==
      ==this====.====walk====(====value====);== ==//== ==核心就是循环对象==
    ==}==
  }
  ==observeArray====(====data====) {== ==//== ==递归遍历数组,对数组内部的对象再次重写== ==[[]]  [{}]==
    ==// vm.arr[0].a = 100;==
    ==// vm.arr[0] = 100;==
    ==//== ==数组里面如果是引用类型那么是响应式的==
    ==data====.====forEach====(====item== ===>== ==observe====(====item====));==
  }
  walk(data) {
    // 使用defineProperty重新定义
    Object.keys(data).forEach(key => {
      defineReactive(data, key, data[key])
    })
  }
}
新建observe/array.js文件:
主要逻辑:

如果变异方法是对数组元素进行新增,我们要对新增的元素调用数组的observeArray方法进行新值的劫持;

如果变异方法是对数组元素进行删除,我们不用处理;
这里注意使用…args将参数转化为数组;
要让这arrayMethods方法可以获取到Observe实例的observeArray方法;
class Observer {
  constructor(value) {
    // 不让__ob__ 被遍历到
    // value.__ob__ = this; // 我给对象和数组添加一个自定义属性
    ==Object====.====defineProperty====(====value====,== =='__ob__'====,== =={==
      ==value:== ==this====,==
      ==enumerable:== ==false== ==//== ==标识这个属性不能被列举出来,不能被循环到==
    ==})==
    // 核心是循环对象
    if (isArray(value)) {
      // 更改数组原型方法, 如果是数组 我就改写数组的原型链
      value.__proto__ = arrayMethods; // 重写数组的方法
      this.observeArray(value);
    } else {
      this.walk(value); // 核心就是循环对象
    }
  }
  observeArray(data) { // 递归遍历数组,对数组内部的对象再次重写 [[]]  [{}]
    // vm.arr[0].a = 100;
    // vm.arr[0] = 100;
    data.forEach(item => observe(item)); // 数组里面如果是引用类型那么是响应式的
  }
  walk(data) {
    // 使用defineProperty重新定义
    Object.keys(data).forEach(key => {
      defineReactive(data, key, data[key])
    })
  }
}
为了observe方法调用时的死循环,我们在上面的代码中定义==enumerable:== ==false==
此外,添加以下代码,防止属性被重复劫持;
export function observe(value) {
  if (!isObject(value)) return;
  ==if== ==(====value====.====__ob__====) {==
    ==return====;== ==//== ==一个对象不需要重新被观测==
  ==}==
  // 利用new Class方式的特点
  // 调用函数
  // 返回一个实例
  // 这个实例我们加上了很多实例属性和方法
  return new Observer(value)
}
// 获取数组的老的原型方法
let oldArrayPrototype = Array.prototype;
// 让arrayMethods 通过__proto__ 能获取到数组的方法
// 相当于 arrayMethods.__proto__ == oldArrayPrototype
export let arrayMethods = Object.create(oldArrayPrototype);
// 只有这七个方法 可以导致数组发生变化
let methods = [
    'push',
    'shift',
    'pop',
    'unshift',
    'reverse',
    'sort',
    'splice'
]
methods.forEach(method => {
    arrayMethods[method] = function (...args) {
        // 数组新增的属性 要看一下是不是对象,如果是对象 继续进行劫持
        // 需要调用数组原生逻辑
        oldArrayPrototype[method].call(this, ...args)
        // todo... 可以添加自己逻辑 函数劫持 切片
        let inserted = null;
        let ob = this.__ob__;// this指向数组实例,即调用此方法的数组
        switch (method) {
            // 修改 删除  添加  arr.splice(0,0,100,200,300)
            case 'splice':
                // splice方法从第三个参数起 是增添的新数据
                inserted = args.slice(2);
                break;
            case 'push':
            case 'unshift':
                // 调用push 和 unshift 传递的参数就是新增的逻辑
                inserted = args;
                break;
        }
        // inserted[] 遍历数组 看一下它是否需要进行劫持
        if (inserted) ob.observeArray(inserted)
    }
});

所以,如果数组使用变异方法新增了元素,这些新增的元素转换成一个数组,然后会偏历其中的每个元素,并对其调用observe方法,也就是如果是对象,就进行截持,基本类型不做处理;

总结:
New Observer中的重要逻辑:

给每个传入的值,也就是对象类型的值,加上__ob__属性,表示已经被观测;同时,通过每个值的__ob__属性,就能获取Observer对象,也就能获取Observer对象的所有属性和方法;

再判断传入的值是对象还是数组,如果是对象,遍历对象的属性值,然后分别添加get和set方法拦截对属性的访问和修改;

如果是数组,修改这个数组的原型,这样当调用这个数组的变异方法时,会调用我们改写过的变异方法;其中这些变异方法如果是给数组新增元素,我们把新增的元素用数组进行包装,然后调用obsever对象的observerArray方法,也就是对每个数组元素进行调用observer方法进行观测,如果其中的元素是数组,我们再递归调用observerArray方法;

本站总访问 — 次