你应该自己实现一个 watch 看不懂找我

943 阅读3分钟

这是我参与8月更文挑战的第3天,活动详情查看:8月更文挑战

vue watch 是一个非常值得借鉴的一个功能点,在你写插件或者写小demo的时候用上就非常漂亮了, 能提升你的编码速度

watch

  • 咱们就实现一个自己的 watch
  • 实现数据监听

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

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

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

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

  1. 定义了一个字符串类型myString
  2. 定义一个boolean 类型的myBoolean
  3. 定义一个object类型的myObject
//相关代码入下
class Observe {
  constructor() {
    this.$data = {
      myString: 'juejin',
      myBoolean: true,
      myObject: {name: 'juejin'}
    }
  
    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 函数
class Observe {
  constructor() {
    this.$data = {
      myString: 'juejin',
      myBoolean: true,
      myObject: {name: 'juejin'}
    }
  
    this.$watch = {
       myString: this.myStringFn,
       myBoolean: this.myBooleanFn,
       myObject: this.myObjectFn
    }
    
     //绑定
     this.observer(this.$data);
  }
  
  myStringFn(d,old) {
   console.log('我是掘金watch监听的string类型', d)
  }
  
  myBooleanFn(d,old) {
    console.log('我是掘金watch监听的boolean类型', d)
  }
  
  myObjectFn(d, old) {
    console.log('我是掘金watch监听的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
    });
  }
}

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

let observer = new Observer();
//更改string
observer.myString = 'jun-jin-test';
//输出: 我是掘金watch监听的string类型 jun-jin-test

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

//更改对象
observer.myObject = {
    name: '掘金文本'
};
//输出:我是掘金watch监听的object类型 {name: "掘金文本"}

验证通过

  • 输出正确

对数组监听实现

大家都是知道Object.defineProperty对数组监听不到得是那么怎么实现呢?

  • vue就是重写数组几个原型得方法然后来实现数组得派发机制得

重写数组得原型上得方法,然后这些方法被调用了 就能监听到了 然而实现派发机制

  • 代码如下
  1. 更改observer 函数
  2. 改写数组原型得方法
  3. 添加一些属性辅助数组得派发机制
  4. 新增$data/$watch 对数组得绑定
class Observe {
  constructor() {
    this.$data = {
      myString: 'juejin',
      myBoolean: true,
      myObject: {name: 'juejin'},
      myArray: ['掘金', '三原']
    }
  
    this.$watch = {
       myString: this.myStringFn,
       myBoolean: this.myBooleanFn,
       myObject: this.myObjectFn,
       myArray: this.myArrayFn,
    }
    
     //绑定
     this.observer(this.$data);
  }
  
  myStringFn(d,old) {
   console.log('我是掘金watch监听的string类型', d)
  }
  
  myBooleanFn(d,old) {
    console.log('我是掘金watch监听的boolean类型', d)
  }
  
  myObjectFn(d, old) {
    console.log('我是掘金watch监听的object类型', d)
  }
  
  myArrayFn(d,old) {
   console.log('我是掘金watch监听的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
    });
  }
}

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

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

验证数组方法成功

  • 验证成功

结束语

  • 大家好,我是三原
  • 多谢您的观看,希望对您有所帮助
  • 有问题可以一块沟通啊

源码和测试代码

<!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: 'juejin',
      myBoolean: true,
      myObject: {name: 'juejin'},
      myArray: ['掘金', '三原']
    }
  
    this.$watch = {
       myString: this.myStringFn,
       myBoolean: this.myBooleanFn,
       myObject: this.myObjectFn,
       myArray: this.myArrayFn,
    }
    
     //绑定
     this.observer(this.$data);
  }
  
  myStringFn(d,old) {
   console.log('我是掘金watch监听的string类型', d)
  }
  
  myBooleanFn(d,old) {
    console.log('我是掘金watch监听的boolean类型', d)
  }
  
  myObjectFn(d, old) {
    console.log('我是掘金watch监听的object类型', d)
  }
  
  myArrayFn(d,old) {
   console.log('我是掘金watch监听的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 = 'jun-jin-test';
//输出: 我是掘金watch监听的string类型 jun-jin-test

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

//更改对象
observer.myObject = {
    name: '掘金文本'
};
//输出:我是掘金watch监听的object类型 {name: "掘金文本"}

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