Skip to content

监听者模式

TIP

什么是监听者模式?简单来说监听者模式就是监听一个对象、数组等发生变化立刻做出回调。vue watch 是一个非常值得借鉴的一个功能点, 下面咱们就基于vue watch实现一个监听者模式。

对对象和字符串的监听实现

  • 设置数据绑定的值,和对应的派发函数 $data/$watch
  • 利用 Object.defineProperty 的get和set 实现对数据的拦截和派发

    TIP

    先定义函数Observe $data 是咱们的数据 $watch 是咱们做数据监听触发对用的函数(相当于vue watch的handler())

数据定义和定义对应的派发函数

  1. 定义了一个字符串类型myString
  2. 定义一个boolean 类型的myBoolean
  3. 定义一个object类型的myObject
javascript
//相关代码入下
class Observe {
  constructor() {
    this.$data = {
      myString: "CSC",
      myBoolean: true,
      myObject: { name: "CSC" },
    };

    this.$watch = {
      myString: this.myStringFn,
      myBoolean: this.myBooleanFn,
      myObject: this.myObjectFn,
    };
  }

  myStringFn() {}

  myBooleanFn() {}

  myObjectFn() {}
}

利用 Object.defineProperty 的get和set 实现对数据的拦截和派发

先介绍下Object.defineProperty MDN

  1. Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象

  2. 语法Object.defineProperty(obj, prop, descriptor)

  3. configurable: 当且仅当该属性的 configurable 键值为 true 时,该属性的描述符才能够被改变,同时该属性也能从对应的对象上被删除

  4. enumerable: 当且仅当该属性的 enumerable 键值为 true 时,该属性才会出现在对象的枚举属性中

  5. value: 该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等)

  6. writable: 当且仅当该属性的 writable 键值为 true 时,属性的值,也就是上面的 value,才能被改变

  7. get: 属性的 getter 函数,如果没有 getter,则为 undefined。当访问该属性时,会调用此函数。

  8. set: 属性的 setter 函数,如果没有 setter,则为 undefined。当属性值被修改时,会调用此函数。

  • 了解过属性该开始咱们的代码了如下
  • 增加observerdefineProperty 函数
javascript
class Observe {
  constructor() {
    this.$data = {
      myString: "CSC",
      myBoolean: true,
      myObject: { name: "CSC" },
    };

    this.$watch = {
      myString: this.myStringFn,
      myBoolean: this.myBooleanFn,
      myObject: this.myObjectFn,
    };

    //绑定
    this.observer(this.$data);
  }

  myStringFn(d, old) {
    console.log("我是CSCwatch监听的string类型", d);
  }

  myBooleanFn(d, old) {
    console.log("我是CSCwatch监听的boolean类型", d);
  }

  myObjectFn(d, old) {
    console.log("我是CSCwatch监听的object类型", d);
  }

  observer(data) {
    if (typeof data !== "object" || data == null) {
      return;
    }
    //循环对象绑定
    for (let key in data) {
      this.defineProperty(key);
    }
  }

  defineProperty(_key) {
    Object.defineProperty(this, _key, {
      get: function () {
        return this.$data[_key];
      },

      set: function (val) {
        const oldVal = cloneDeep(this.$data[_key]);
        if (oldVal === val) return val;
        this.$data[_key] = val;
        if (!!this.$watch[_key] && typeof this.$watch[_key] === "function") {
          this.$watch[_key].call(this, val, oldVal);
        }

        return val;
      },

      enumerable: true,
      configurable: true,
    });
  }
}

验证对象、字符串是否监听到并派发对应得函数

javascript
let observer = new Observer();
//更改string
observer.myString = "CSC-test";
//输出: 我是CSCwatch监听的string类型 CSC-test

//更改boolean
observer.myBoolean = false;
//输出:我是CSCwatch监听的boolean类型 false

//更改对象
observer.myObject = {
  name: "CSC文本",
};
//输出:我是CSCwatch监听的object类型 {name: "CSC文本"}

验证通过

  • 输出正确

对数组监听实现

TIP

Object.defineProperty 对数组监听不到得, 那在vue中是那么怎么实现呢?vue重写了数组几个原型得方法然后来实现数组得派发机制。

  • 代码如下
  1. 更改observer 函数
  2. 改写数组原型得方法
  3. 添加一些属性辅助数组得派发机制
  4. 新增$data/$watch 对数组得绑定
javascript
class Observe {
  constructor() {
    this.$data = {
      myString: "CSC",
      myBoolean: true,
      myObject: { name: "CSC" },
      myArray: ["CSC", "CSC"],
    };

    this.$watch = {
      myString: this.myStringFn,
      myBoolean: this.myBooleanFn,
      myObject: this.myObjectFn,
      myArray: this.myArrayFn,
    };

    //绑定
    this.observer(this.$data);
  }

  myStringFn(d, old) {
    console.log("我是CSCwatch监听的string类型", d);
  }

  myBooleanFn(d, old) {
    console.log("我是CSCwatch监听的boolean类型", d);
  }

  myObjectFn(d, old) {
    console.log("我是CSCwatch监听的object类型", d);
  }

  myArrayFn(d, old) {
    console.log("我是CSCwatch监听的Array类型", d);
  }

  observer(data) {
    if (typeof data !== "object" || data == null) {
      return;
    }

    let _this = this;
    let originalProto = Array.prototype;
    // 先克隆一份Array的原型出来
    let arrayProto = Object.create(originalProto);

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

    methodsToPatch.forEach((method) => {
      arrayProto[method] = function () {
        console.log("数组方法被调用了");
        const oldVal = _this.$data[this.notify];
        originalProto[method].apply(this, arguments);

        //实现派发机制
        _this.$watch[this.notify].call(_this, this, oldVal);
      };
    });

    for (let key in data) {
      if (Array.isArray(data[key])) {
        //给数组方法上绑定一个notify 指定需要得key
        arrayProto.notify = key;
        //重写array 上的__proto__
        data[key].__proto__ = arrayProto;
      }

      this.defineProperty(key);
    }
  }

  defineProperty(_key) {
    Object.defineProperty(this, _key, {
      get: function () {
        return this.$data[_key];
      },

      set: function (val) {
        const oldVal = cloneDeep(this.$data[_key]);
        if (oldVal === val) return val;
        this.$data[_key] = val;
        if (!!this.$watch[_key] && typeof this.$watch[_key] === "function") {
          this.$watch[_key].call(this, val, oldVal);
        }

        return val;
      },

      enumerable: true,
      configurable: true,
    });
  }
}

验证数组是否监听到并派发对应得函数

javascript
//更改数组
observer.myArray.push("push进来的");
//输出:数组方法被调用了  我是CSCwatch监听的Array类型 (3) ["CSC", "CSC", "push进来的"]

验证数组方法成功

  • 验证成功

源码和测试代码

html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body></body>
</html>

<script>
  class Observer {
    constructor() {
      this.$data = {
        myString: "CSC",
        myBoolean: true,
        myObject: { name: "CSC" },
        myArray: ["CSC", "CSC"],
      };

      this.$watch = {
        myString: this.myStringFn,
        myBoolean: this.myBooleanFn,
        myObject: this.myObjectFn,
        myArray: this.myArrayFn,
      };

      //绑定
      this.observer(this.$data);
    }

    myStringFn(d, old) {
      console.log("我是CSCwatch监听的string类型", d);
    }

    myBooleanFn(d, old) {
      console.log("我是CSCwatch监听的boolean类型", d);
    }

    myObjectFn(d, old) {
      console.log("我是CSCwatch监听的object类型", d);
    }

    myArrayFn(d, old) {
      console.log("我是CSCwatch监听的Array类型", d);
    }

    observer(data) {
      if (typeof data !== "object" || data == null) {
        return;
      }

      let _this = this;
      let originalProto = Array.prototype;
      // 先克隆一份Array的原型出来
      let arrayProto = Object.create(originalProto);

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

      methodsToPatch.forEach((method) => {
        arrayProto[method] = function () {
          console.log("数组方法被调用了");
          const oldVal = _this.$data[this.notify];
          originalProto[method].apply(this, arguments);

          //实现派发机制
          _this.$watch[this.notify].call(_this, this, oldVal);
        };
      });

      for (let key in data) {
        if (Array.isArray(data[key])) {
          //给数组方法上绑定一个notify 指定需要得key
          arrayProto.notify = key;
          //重写array 上的__proto__
          data[key].__proto__ = arrayProto;
        }

        this.defineProperty(key);
      }
    }

    defineProperty(_key) {
      Object.defineProperty(this, _key, {
        get: function () {
          return this.$data[_key];
        },

        set: function (val) {
          const oldVal = this.$data[_key];
          if (oldVal === val) return val;
          this.$data[_key] = val;
          if (!!this.$watch[_key] && typeof this.$watch[_key] === "function") {
            this.$watch[_key].call(this, val, oldVal);
          }

          return val;
        },

        enumerable: true,
        configurable: true,
      });
    }
  }

  let observer = new Observer();
  //更改string
  observer.myString = "CSC-test";
  //输出: 我是CSCwatch监听的string类型 CSC-test

  //更改boolean
  observer.myBoolean = false;
  //输出:我是CSCwatch监听的boolean类型 false

  //更改对象
  observer.myObject = {
    name: "CSC文本",
  };
  //输出:我是CSCwatch监听的object类型 {name: "CSC文本"}

  //更改数组
  observer.myArray.push("push进来的");
  //输出:数组方法被调用了  我是CSCwatch监听的Array类型 (3) ["CSC", "CSC", "push进来的"]
</script>