论如何监听对象某个属性的变化

栏目: 后端 · 发布时间: 5年前

内容简介:本文介绍了两种监听对象某个属性的变化的思路, 分别是利用私有属性以及利用观察者模式,建议有经验的读者直接阅读最后的实现部分 :)上一篇文章我们谈到了如何监听对象的变化。下面我们来探究如何用

本文介绍了两种监听对象某个属性的变化的思路, 分别是利用私有属性以及利用观察者模式,建议有经验的读者直接阅读最后的实现部分 :)

前景回顾

上一篇文章我们谈到了如何监听对象的变化。

下面我们来探究如何用 $watch 方法中的callback来替换 console.warn(newVal, oldVal) , 以及如何只监听对象的某个属性的变化。

另外本文只讨论 Vue.prototype.$watch keypath的写法,即 this.$watch('a.b.c', () => {});

function proxy(obj) {
  const handler = {
    get(target, prop) {
      try {
        return new Proxy(target[prop], handler);
      } catch (error) {
        return target[prop];
      }
    },
    set(target, prop, newVal) {
      const oldVal = target[prop];
      if (oldVal !== newVal) {
        // 如何替换这个强耦合的函数
        console.warn(newVal, oldVal);
      }
      target[prop] = newVal;
      return true;
    },
  };

  return new Proxy(obj, handler);
}

const obj = proxy({
  a: 'a',
  b: 'b',
  c: 'c',
});

// 以及如何做到当obj.a改变时只触发第一个callback
$watch(obj, 'a', (val, oldVal) => {
  console.warn('watch obj.a: ', val, oldVal);
});
$watch(obj, 'b', (val, oldVal) => {
  console.warn('watch obj.b: ', val, oldVal);
});
复制代码

思路

关于私有属性

有个十分简单的思路: 把callback和要监听的属性值, 作为被监听对象某一层级的私有属性注入

// 监听obj.a和obj.b
const obj = {
  a: 'a',
  b: 'b',
  c: 'c',
  // 因为我们需要监听两个属性,所以需要使用集合
  __waters__: [{
    key: 'a',
    cb: () => {},
  }, {
    key: 'b',
    cb: () => {},
  }],
};

// 对于多层级的被监听对象, __watchers__挂载在不同的层级下
const obj = {
  o: {
    name: 'obj',
    __watchers__: [{
      key: 'name',
      cb: () => {},
    }],
  },
  odeep: {
    path: {
      name: 'obj deep',
      __watchers__: [{
        key: 'name',
        cb: () => {},
      }],
    },
  },
};
复制代码

关于观察者模式

先让我们看看维基百科是怎么说的:

The observer pattern is a software design pattern in which an object, called the subject, maintains a list of its dependents, called observers, and notifies them automatically of any state changes, usually by calling one of their methods.

也就是说subject用来维护依赖列表, 每个依赖都是一个observer。当依赖列表中的某一项发生了变化,就自动通知subject自身状态的变更。

function proxy(obj) {
  const handler = {
    get(target, prop) {
      // 设置observer(依赖)
      return target[prop]; // 不递归监听
    },

    set(target, prop, newVal) {
      const val = target[prop];
      if (newVal !== val) {
        target[prop] = newVal;
        // observer通知自身状态的改变, 即调用callback
      }
      return true;
    },
  };

  return new Proxy(obj, handler);
}
复制代码

但是callback在 $watch 函数中, 如何传递给observer, 并在被监听对象变化时调用呢?

我们可以利用一个全局变量,在访问变量的时候设置为 $watch 函数的callback, 访问结束后置空。

let DepTarget = null;
function $watch(obj, path, cb) {
  DepTarget = cb;
  // 访问obj,自动调用get方法实现依赖注入
  DepTarget = null;
}

class Dep {
  constructor() {
    this.subs = new Set();
  }
  add(sub) {
    this.subs.add(sub);
  }
  notify() {
    this.subs.forEach((sub) => {
      // sub需要存储oldVal和newVal, 当且仅当oldVal不等于newVal时调用callback
      sub.update();
    });
  }
}
复制代码

实现

关于私有属性

对于这个思路而言,我们只需要找出需要监听的属性的上一层级, 不难抽象出下面的函数:

function parseParentPath(path, obj) {
  if (/[^\w.$]/.test(path)) {
    return {};
  }

  let segs = path.split('.');
  // 监听属性的上一层,所以是length - 1
  segs = segs.slice(0, segs.length - 1);
  for (let i = 0; i < segs.length; i += 1) {
    if (!obj) {
      return {};
    }
    obj = obj[segs[i]];
  }

  return obj;
}
复制代码

那么 $watch 也不难写出来了

function $watch(obj, path, cb) {
  const parent = parseParentPath(path, obj);
  // 限于篇幅,边界判断还请自行脑补 :)
  const segs = path.split('.');
  const key = segs[segs.length - 1];

  if (!parent.__watchers__) {
    Object.defineProperty(parent, '__watchers__', {
      value: [],
      configurable: true,
    });
  }
  parent.__watchers__.push({ key, cb });
}

const handler = {
  get(target, prop) {
    try {
      return new Proxy(target[prop], handler);
    } catch (error) {
      return target[prop];
    }
  },

  set(target, prop, newVal) {
    const oldVal = target[prop]; 
    const { __watchers__ } = target;
    
    if (__watchers__) {
      const current = __watchers__.find(e => e.key === prop);
      if (oldVal !== newVal && current && typeof current.cb === 'function') {
        current.cb(newVal, oldVal);
      }
    }
    
    target[prop] = newVal;
    return true;
  },
};

obj = new Proxy(obj, handler);
复制代码

好了,让我们来试试吧!

let obj = {
  b: true,
  o: { name: 'obj', age: 18 },
  a: ['a', 'b', 'c'],
  odeep: {
    path: {
      name: 'obj deep',
      value: [],
    },
  },
};

$watch(obj, 'b', (newVal, oldVal) => {
  console.error('watch b: ', newVal, oldVal);
});
$watch(obj, 'o.name', (newVal, oldVal) => {
  console.error('watch o.name: ', newVal, oldVal);
});
$watch(obj, 'odeep.path.name', (newVal, oldVal) => {
  console.error('watch odeep.path.name: ', newVal, oldVal);
});

setTimeout(() => {
  // 当然不会有什么问题
  obj.o.name = 'new obj';
  obj.b = false;
  obj.odeep.path.name = 'new obj deep';
}, 1000);
复制代码

但是这样的写法存在一些局限性

$watch(obj, 'odeep', (newVal, oldVal) => {
  console.error('watch odeep: ', newVal, oldVal);
}, { deep: true });

setTimeout(() => {
  // 对于{ deep: true }, 需要在对象的每个层级添加__watchers__属性,显然不太合适
  obj.odeep.path.name = 'new obj deep';
}, 1000);
复制代码

关于观察者模式

我们利用sub来存储oldVal和newVal, 并将 $watch 的逻辑写入sub的 get 方法中

class Sub {
  constructor(obj, path, cb) {
    this.obj = obj;
    this.path = path;
    this.cb = cb;
    this.value = this.get();
  }

  get() {
    // 因为Dep的add方法传参为sub, 因此全局变量设置为当前sub
    DepTarget = this;
    // 访问obj
    const value = parsePath(this.path)(this.obj);
    DepTarget = null;
    
    return value;
  }

  update() {
    const value = this.get();

    if (this.value !== value) {
      const oldVal = this.value;
      this.value = value;
      this.cb.call(this.obj, value, oldVal);
    }
  }
}
复制代码

下面是完整的例子:

let DepTarget = null;

class Sub {
  constructor(obj, path, cb) {
    this.obj = obj;
    this.path = path;
    this.cb = cb;
    this.value = this.get();
  }

  get() {
    DepTarget = this;
    // 访问obj
    const value = parsePath(this.path)(this.obj);
    DepTarget = null;
    return value;
  }

  update() {
    const value = this.get();
    if (this.value !== value) {
      const oldVal = this.value;
      this.value = value;
      this.cb.call(this.obj, value, oldVal);
    }
  }
}

class Dep {
  constructor() {
    this.subs = new Set();
  }
  add(sub) {
    this.subs.add(sub);
  }
  notify() {
    this.subs.forEach((sub) => {
      // sub需要存储oldVal和newVal, 当且仅当oldVal不等于newVal时调用callback
      sub.update();
    });
  }
}

function proxy(obj) {
  const dep = new Dep();
  
  const handler = {
    get(target, prop) {
      if (DepTarget) {
        dep.add(DepTarget);
      }
      // 不递归监听
      return target[prop];
    },
    set(target, prop, newVal) {
      const val = target[prop];
      if (newVal !== val) {
        target[prop] = newVal;
        dep.notify();
      }
      return true;
    },
  };

  return new Proxy(obj, handler);
}

function parsePath(path) {
  if (/[^\w.$]/.test(path)) {
    return;
  }
  var segs = path.split('.');
  return function(obj) {
    for (let i = 0; i < segs.length; i += 1) {
      if (!obj) {
        return;
      }
      obj = obj[segs[i]];
    }
    return obj;
  };
}

const obj = proxy({
  a: 'a',
  b: 'b',
  o: { name: 'a', age: 18 },
  arr: [1, 2],
});

function $watch(obj, path, cb) {
  return new Sub(obj, path, cb);
}

$watch(obj, 'a', (val, newVal) => {
  console.warn('watch a: ', val, newVal);
});
$watch(obj, 'b', (val, newVal) => {
  console.warn('watch b: ', val, newVal);
});
$watch(obj, 'o.age', (val, newVal) => {
  console.warn('watch o.age: ', val, newVal);
});
$watch(obj, 'arr', (val, newVal) => {
  console.warn('watch arr: ', val, newVal);
});

setTimeout(() => {
  obj.b = 'new b';
  obj.o.age -= 1;
  // vue会打印相同的值, 你会发现我们的实现不会打印
  obj.arr.push(3);
  obj.arr = [3];
}, 1000);
复制代码

细心的你应该发现了,我们没有实现 Vue.prototype.$watch 常用的 { deep: true } 参数, 限于篇幅, 笔者决定还是放在下一篇文章介绍 :)


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Open Data Structures

Open Data Structures

Pat Morin / AU Press / 2013-6 / USD 29.66

Offered as an introduction to the field of data structures and algorithms, Open Data Structures covers the implementation and analysis of data structures for sequences (lists), queues, priority queues......一起来看看 《Open Data Structures》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具