【问题标题】:How to observe property value changes of a third party object?如何观察第三方对象的属性值变化?
【发布时间】:2022-01-06 04:38:27
【问题描述】:

我想观察第三方对象的属性何时发生变化。我正在采取分配自定义设置器的方法,但我下面的console.log 从未被调用过。这是为什么?有更好的方法吗?

const foo = { a: 1, b: 2 };

Object.assign(foo, {
  set user(user) {
    foo.user = user;
    console.log(">>>>>> user was changed", user);
  },
});

// Desired behaviour
foo.user = "asdf"; // >>>>>> user was changed asdf
delete foo.user; // >>>>>> user was changed undefined
foo.user = "asdf1" // >>>>>> user was changed asdf1

请注意,我需要对 foo 进行变异,我不能将代理包装在 foo 周围并返回它,因为它是一个第三方库,会在内部对 .user 进行变异

【问题讨论】:

  • 这是一个很好的Proxy用例
  • 不,我不能在这里使用代理,因为据我所知,代理不会修改底层目标@Reyno 和 Pavan J
  • 但是代理指向同一个对象所以改变代理对象也会改变foo?
  • 是的,很公平@Andreas 我很感激它被重新打开了????
  • @Reyno 如果你不能改变const foo = { a: 1, b: 2 };这一行,那么你就不能真正使用代理。

标签: javascript object properties observable propertydescriptor


【解决方案1】:

我找到了一种方法,虽然它很老套

const foo = { a: 1, b: 2 };

let underlyingValue = foo.user

Object.defineProperty(foo, "user", {
  get() {
    return underlyingValue
  },
  set(user) {
    underlyingValue = user;
    console.log(">>>>>> user was changed", user);
  },
  enumerable: true
});

foo.user = "asdf";
console.log(foo)

我已经把它变成了下面的一个通用函数 ?

/** Intercepts writes to any property of an object */
function observeProperty(obj, property, onChanged) {
  const oldDescriptor = Object.getOwnPropertyDescriptor(obj, property);
  let val = obj[property];
  Object.defineProperty(obj, property, {
    get() {
      return val;
    },
    set(newVal) {
      val = newVal;
      onChanged(newVal);
    },
    enumerable: oldDescriptor?.enumerable,
    configurable: oldDescriptor?.configurable,
  });
}

// example usage ?
const foo = { a: 1 };
observeProperty(foo, "a", (a) => {
  console.log("a was changed to", a);
});
foo.a = 2; // a was changed to  2

Also available in typescript

? 编辑:如果属性被删除,这将中断,例如delete foo.user。观察者将被移除,回调将停止触发。您需要重新连接它。

【讨论】:

  • 确实 "...虽然很老套。" ...在这种情况下,user 不像ab 那样可枚举。以及观察除user 之外的其他属性怎么样。后者是特例还是 OP 需要某种更通用的观察方法?
  • foo 不是您可能需要重新表述您的评论的属性?
  • 好点,我会让它成为可枚举的。至于通用方法,是的,在折叠的 sn-p 中
【解决方案2】:

@david_adler ...当我发表评论时...

“后者是特例还是 OP 需要某种更通用的观察方法?”

...我想到了最通用的解决方案,将现有对象完全更改/变异为自身的可观察变体。

这样的解决方案也将更接近 OP 的要求......

“我想观察第三方对象的属性何时发生变化”

因此,下一个提供的方法保留了对象的外观和行为,也不会引入额外的(例如基于Symbol)的键。

function mutateIntoObservableZombie(obj, handlePropertyChange) {
  const propertyMap = new Map;

  function createAccessors(keyOrSymbol, initialValue, handler) {
    return {
      set (value) {
        propertyMap.set(keyOrSymbol, value);
        handler(keyOrSymbol, value, this);
        return value;
      },
      get () {
        return propertyMap.has(keyOrSymbol)
          ? propertyMap.get(keyOrSymbol)
          : initialValue;
      },
    };
  }
  function wrapSet(keyOrSymbol, proceed, handler) {
    return function set (value) {
      handler(keyOrSymbol, value, this);
      return proceed.call(this, value);
    };
  }
  function createAndAssignObservableDescriptor([keyOrSymbol, descriptor]) {
    const { value, get, set, writable, ...descr } = descriptor;

    if (isFunction(set)) {
      descr.get = get;
      descr.set = wrapSet(keyOrSymbol, set, handlePropertyChange);
    }
    if (descriptor.hasOwnProperty('value')) {
      Object.assign(descr, createAccessors(keyOrSymbol, value, handlePropertyChange));
    }
    Object.defineProperty(obj, keyOrSymbol, descr);
  }
  const isFunction = value => (typeof value === 'function');

  if (isFunction(handlePropertyChange)) {
    const ownDescriptors = Object.getOwnPropertyDescriptors(obj);
    const ownDescrSymbols = Object.getOwnPropertySymbols(ownDescriptors);

    Object
      .entries(ownDescriptors)
      .forEach(createAndAssignObservableDescriptor);

    ownDescrSymbols
      .forEach(symbol =>
        createAndAssignObservableDescriptor([symbol, ownDescriptors[symbol]])
      );
  }
  return obj;
}


// third party object (closed/inaccessible code)
const foo = { a: 1, b: 2 };


// custom changes already.
foo.userName = '';
foo.userLoginName = '';

const userNick = Symbol('nickname');

foo[userNick] = null;

console.log('`foo` before descriptor change ...', { foo });

mutateIntoObservableZombie(foo, (key, value, target) => {
  console.log('property change ...', { key, value, target });
});
console.log('`foo` after descriptor change ...', { foo });

foo.a = "foo bar";
foo.b = "baz biz";

console.log('`foo` after property change ...', { foo });

foo.userName = '****';
foo.userLoginName = '************@**********';

console.log('`foo` after property change ...', { foo });

foo[userNick] = 'superuser';

console.log('`foo` after symbol property change ...', { foo });
.as-console-wrapper { min-height: 100%!important; top: 0; }

编辑

由于上述方法已经实现了通用和模块化,因此可以很容易地将其重构为一个函数,该函数允许精确定义将要观察哪些属性,基于stringsymbol。 ..

function observePropertyChange(obj, keysAndSymbols, handlePropertyChange) {
  const propertyMap = new Map;

  function createAccessors(keyOrSymbol, initialValue, handler) {
    return {
      set (value) {
        propertyMap.set(keyOrSymbol, value);
        handler(keyOrSymbol, value, this);
        return value;
      },
      get () {
        return propertyMap.has(keyOrSymbol)
          ? propertyMap.get(keyOrSymbol)
          : initialValue;
      },
    };
  }
  function wrapSet(keyOrSymbol, proceed, handler) {
    return function set (value) {
      handler(keyOrSymbol, value, this);
      return proceed.call(this, value);
    };
  }
  function createAndAssignObservableDescriptor(keyOrSymbol, descriptor) {
    const { value, get, set, writable, ...descr } = descriptor;

    if (isFunction(set)) {
      descr.get = get;
      descr.set = wrapSet(keyOrSymbol, set, handlePropertyChange);
    }
    if (descriptor.hasOwnProperty('value')) {
      Object.assign(descr, createAccessors(keyOrSymbol, value, handlePropertyChange));
    }
    Object.defineProperty(obj, keyOrSymbol, descr);
  }
  const isString = value => (typeof value === 'string');
  const isSymbol = value => (typeof value === 'symbol');
  const isFunction = value => (typeof value === 'function');

  if (isFunction(handlePropertyChange)) {

    const ownDescriptors = Object.getOwnPropertyDescriptors(obj);
    const identifierList = (Array
      .isArray(keysAndSymbols) && keysAndSymbols || [keysAndSymbols])
      .filter(identifier => isString(identifier) || isSymbol(identifier));

    identifierList
      .forEach(keyOrSymbol =>
        createAndAssignObservableDescriptor(keyOrSymbol, ownDescriptors[keyOrSymbol])
      );
  }
  return obj;
}


// third party object (closed/inaccessible code)
const foo = { a: 1, b: 2 };


// custom changes already.
foo.userName = '';
foo.userLoginName = '';

const userNick = Symbol('nickname');

foo[userNick] = null;

console.log('`foo` before descriptor change ...', { foo });

observePropertyChange(
  foo,
  ['b', 'userLoginName', userNick],
  (key, value, target) => { console.log('property change ...', { key, value, target }); },
);
console.log('`foo` after descriptor change ...', { foo });

foo.a = "foo bar";
foo.b = "baz biz";

console.log('`foo` after property change ...', { foo });

foo.userName = '****';
foo.userLoginName = '************@**********';

console.log('`foo` after property change ...', { foo });

foo[userNick] = 'superuser';

console.log('`foo` after symbol property change ...', { foo });
.as-console-wrapper { min-height: 100%!important; top: 0; }

【讨论】:

  • @david_adler ...当我评论时 ... “后者是一个特例还是 OP 需要某种更通用的观察方法?” ...我想到一个可以想出的最通用的解决方案,将现有对象完全更改/变异为自身的可观察变体。这样的解决方案也将更接近 OP 确实要求的... “我想观察第三方对象的属性何时发生变化” ...因此上述提供的方法保持对象的外观和行为,也不会引入其他(例如基于Symbol)的键。
  • 是的好方法,不需要符号属性,已将我的实现更新为与您的更相似。但是,在我的特定用例中,我不需要观察所有属性,因此我不想处理观察方法等的复杂性。
  • @david_adler ...在这种情况下,您可能会考虑更精确地描述描述和标题("Observe any property of an object" )。
  • 酷,现在完成了。我认为你的回答可能对其他未来的谷歌人有用
  • @david_adler ...我想不出任何替代方案。这就是为什么我将我的最后一条评论作为最终选择。从我们的想法来看,我认为这是一种基于属性描述符的方法。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2014-04-18
  • 2017-09-07
  • 1970-01-01
  • 2019-09-21
  • 1970-01-01
  • 2018-09-15
  • 2011-05-27
相关资源
最近更新 更多