Vue原理:如何对数组进行数据劫持

1,908 阅读3分钟

这是我参与11月更文挑战的第14天,活动详情查看:2021最后一次更文挑战

Vue在数据劫持中对于数组特殊处理的原因:

使用Object.defineProperty并不会对数据的索引进行劫持,比如我现在有一个数组,长度是100000,如果对每个索引都进行劫持,那么就需要将劫持100000次,所以劫持数组会造成性能问题,性能代价和用户体验收益不成正比

在日常开发过程中,我们也很少会直接通过数组的索引去操作数组,这样是存在问题的,因为数组是会一直在变化的,如果数据改变,那么通过索引获取到的数据并不是期望获得的,大部分都是通过数组的原型方法psuhpop等方法操作,因此将数组的7个可以改变数组本身的函数进行了代理

劫持数组方法

对于数组的处理,专门提取到一个文件src/observe/array.js

看一下对于数组的处理,核心代码在代码中已注释

/**
 * 重写数组的一些方法(会改变数组本身的一些方法)
 */
const methods = [
  "push",
  "pop",
  "shift",
  "unshift",
  "sort",
  "splice",
  "reverse",
];

// 获取数组原型方法
const oldArrMethods = Array.prototype;
// 根据数据原型上的方法全部拷贝
export const arrMethods = Object.create(oldArrMethods);

/**
 * 遍历需要处理的7个方法
 */
methods.forEach((method) => {
  // 劫持7个函数
  arrMethods[method] = function (...args) {
    // 获调用数据原有的方法
    const res = oldArrMethods[method].apply(this, args);

    // 若是调用 push、unshift和splice, 则inserted代表被加入的新的数据
    let inserted;
    // 获取到Observer实例
    let ob = this.__ob__;

    switch (method) {
      case "push":
      case "unshift":
        inserted = args;
        break;

      case "splice": // splice 有新增 删除的功能,
        inserted = args.slice(2); // 截取 新增的数据
        break;
      default:
        break;
    }

    if (inserted) {
      // 将新增属性 继续劫持
      ob.observeArray(inserted);
    }
    
    return res;
  };
});

上述代码核心:

  1. 为何只对pushpop等7个方法进行特殊处理
  2. 为何使用Object.create创建一个新的数组原型方法,这样做可以不影响数组原有的原型函数
  3. 对于数组新增和替换的数据再次进行劫持

Observer改造

既然数组的处理已经完成,接下来便是在src/observe/index.js引入并使用

Observer类的构造中需要做两件事情

  1. 给每一个监控过的对象都增加一个 __ob__ 属性
def(data, "__ob__", this);

def函数做的事情:将数据设置为不可枚举、不可配置


export function def(data, key, value) {
  Object.defineProperty(data, key, {
    enumerable: false,
    configurable: false,
    value,
  });
}

这里设置不能直接使用

data.__ob__ = this;

存在循环调用的风险

  1. 将数组和对象分别处理

对象依旧调用walk函数进行处理,数组则需要特殊处理:

改变数组的原型指向

data.__proto__ = arrMethods;

重新定义一个observeArray函数处理数组

整体代码变为:

class Observer {
  constructor(data) {
    def(data, "__ob__", this);

    if (Array.isArray(data)) { // 处理数组
      data.__proto__ = arrMethods;
      this.observeArray(data);
    } else { // 处理对象
      this.walk(data);
    }
  }

  walk(data) {
    // 保持不变
  }

  observeArray(data) {
    // 监控数组中的对象
    for (let i = 0; i < data.length; i++) {
      observe(data[i]);
    }
  }
}

此时代码对于数组的处理只能处理一维数组,对于多维数组的处理,需要继续处理

function dependArray(val) {
  for (let i = 0; i < val.length; i++) {
    const current = val[i];
    if(Array.isArray(current)) {
      dependArray(current)
    }
  }
}

其调用时机在数据劫持get

get() {
  // 如果数组中 还有数组, 需要将数组中的每一项再收集一下依赖
  if(Array.isArray(val)) {
    dependArray(val)
  }
  return val;
}