# 框架原理

双向数据绑定的直观形式无非是,数据在变化时不再需要开发者手动的去更新视图,而是由视图根据数据的变化“自动”更新。完成上述流程只需要做到以下几点即可:

  • 当前页面视图需要知道依赖了哪些数据。
  • 感知被依赖的数据的变化情况。
  • 数据变化时,自动的“通知”需要更新的视图部分,进行视图更新。

而做到上述几点对应的技术概念如下:

  • 数据依赖的收集
  • 被依赖的数据进行劫持和代理
  • 形成发布/订阅的模式。

# 数据的劫持和代理

感知数据变化的方法很直接,对要感知的数据进行劫持和代理。也就是常说的给数据定义 gettersetter。定义的方法就是通过 Object.defineProperty

let data = {
  stage: "来了来了",
  course: {
    title: "前端开发",
    author: "lucy",
    publishTime: "2022年7月23日",
  },
};

// 使用 Object.defineProperty 对数据进行劫持代理
Object.keys(data).forEach((key) => {
  let currentVal = data[key];

  Object.defineProperty(data, key, {
    enumerable: true, // 可枚举的
    configurable: false, // 可配置的
    get() {
      console.log(
        `getting ${key} value now, value is ${JSON.stringify(currentVal)}`
      );

      return currentVal;
    },
    set(newValue) {
      currentVal = newValue;
      console.log(`setting ${key} value now, setting value is ${newValue}`);
    },
  });
});

// 最后要执行一次,才会走 get 方法
data.course;
// getting course value now, value is {"title":"前端开发","author":"lucy","publishTime":"2022年7月23日"}

data.course.title = "修改title触发set"; // 没办法触发 set

An image

上述方法的缺点是,如果修改对象 data.course 下的数据 title 时,没办法触发对应数据的 setter。因为对象 data.course 没有被代理到。

let data = {
  stage: "来了来了",
  course: {
    title: "前端开发",
    author: "lucy",
    publishTime: "2022年7月23日",
  },
};

const observe = (data, isTrue = false) => {
  if (!data || typeof data !== "object") {
    return;
  }

  Object.keys(data).forEach((key) => {
    let currentVal = data[key];

    // 为子集是 json 的也代理递归处理
    observe(data[key], true);

    if (!isTrue) {
      console.log("key:", key, "value:", currentVal);
    }

    Object.defineProperty(data, key, {
      enumerable: true, // 可枚举的
      configurable: false, // 可配置的
      get() {
        console.log(
          `getting ${key} value now, value is ${JSON.stringify(currentVal)}`
        );

        return currentVal;
      },
      set(newValue) {
        currentVal = newValue;
        console.log(`setting ${key} value now, setting value is ${newValue}`);
      },
    });
  });
};

observe(data, true);

data.course.title = "修改title使set触发";

An image

上述修改 title 时只是简单的修改成了简单的数据类型,而不是复杂的数据类型。如果 title 被赋值了复杂的数据类型,仍然需要实现一次新数据的代理劫持。

data.course.title = { name: "修改title使set触发" };

// 如果是这种数据类型的赋值,仍然需要进行一次数据代理的劫持

# 监听数组的变化

将上述 data 数据中补充数组形式的数据

let data = {
  stage: "来了来了",
  course: {
    title: "前端开发",
    author: "lucy",
    publishTime: "2022年7月23日",
  },
  author: ["lucy", "bobby"],
};

// 监听不到
data.author.push("jojo");

之所监听不到数组的 push 行为,是因为 Object.defineProperty 只是做了数据的代理,而数组的 push 方法是一种行为,Array.property 上挂在的方法并不能触发 data.author 属性值的 setter,因为这并不属于赋值操作。

所以,我们需要重写数组方法,覆盖掉原生的数组方法。

const arrExtend = Object.create(Array.prototype);

const arrMethods = [
  "push",
  "pop",
  "shift",
  "unshift",
  "splice",
  "sort",
  "reverse",
];

arrMethods.forEach((method) => {
  const oldMethod = Array.prototype[method];

  const newMethod = function (...args) {
    oldMethod.apply(this, args);

    console.log(`${method}方法被调用了`);
  };

  arrExtend[method] = newMethod;
});

Array.prototype = Object.assign(Array.prototype, arrExtend);

let array = [1, 2, 3, 4];

array.push(5); // push 方法被调用了

# 代理的升级 Proxy

Proxy 实现数据代理和使用 Object.defineProperty 实现数据代理拦截的对比如下:

  • Object.defineProperty 不能监听数组的变化,需要对数组方法进行重写。
  • Object.defineProperty 必须遍历对象的每个属性,且需要对嵌套结构进行深层次遍历。
  • Proxy 的代理是针对整个对象的,而不是针对对象的某个属性的。所以,不用像 Object.defineProperty 那样必须遍历对象的所有属性。Proxy 只需要做一层代理就可以监听同级结构下所有属性的变化,针对深层结构,递归处理也是需要进行的。
  • Proxy 支持代理数组的变化
  • Proxy 的第二个参数除了可以使用 set 和 get,还可以使用 13 种拦截方法,比 Object.defineProperty 更为强大
  • 使用 Proxy 时,性能将会被底层持续优化;而使用 Object.defineProperty 时,性能已经不再是优化的重点了。