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

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

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

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

前景回顾

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

下面我们来探究如何用 $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 } 参数, 限于篇幅, 笔者决定还是放在下一篇文章介绍 :)


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

查看所有标签

猜你喜欢:

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

商业的常识

商业的常识

申音 / 山西经济出版社 / 2011-7-1 / 35.00元

★为什么美国没有史玉柱,中国没有乔布斯? ★什么是“对的行业”、“错的行业”? ★我们需要什么样的营销? ★老板为什么要读商学院? ★山寨公司还需要管理吗? ★资源问题是个“伪问题”? ★别把商业模式当成葵花宝典 ★给海归技术创业兄弟的九个忠告 ★在一个不伟大的行业里,做一个伟大的公司 ★是什么让互联网遭遇了有史以来最鸡犬不宁的一战?一起来看看 《商业的常识》 这本书的介绍吧!

CSS 压缩/解压工具
CSS 压缩/解压工具

在线压缩/解压 CSS 代码

SHA 加密
SHA 加密

SHA 加密工具

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具