Javascript 中的 KVC 和 KVO 以及变量监听

本文介绍如何在JavaScript中实现类似Objective-C的KVC和KVO功能,通过注入setter方法来监听对象属性的变化,并提供了完整的代码示例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Javascript 中的 KVC 和 KVO 以及变量监听

KVC(Key Value Coding) 和 KVO(Key Value Observing) 是 Objective-C 中的术语(当然 Swift 也是支持的). 说白了, KVC 可以让你像操作 NSDictionary 一样操作你的对象; KVO 可以让你监听你的对象的属性的改变.

所谓的 KVC 在 Javascript 中其实更直观, 下面是 ObjC 的 KVC 语法以及 JS 的类似写法:

Key Value Coding
ObjC JS
// Read a value
[myObject valueForKey:@"myValue"]

// Set a value 
[myObject setValue:@123 forKey:@"myValue"]
// Read a value
myObject['myValue']

// Set a value
myObject['myValue'] = 123

对于 Objective-C 来说, KVC 是实现 KVO 的基础.

Objective-C 中使用 KVO 最常用的方法是使用 addObserver:forKeyPath:options:context: 添加对象某一属性(key path)的观察者. 然后观察者实现 observeValueForKeyPath:ofObject:change:context: 来响应被观察对象的属性的变化. 最后不要忘了使用removeObserver:forKeyPath:删除观察者.

本文侧重点是 Javascript 的属性变化监听, 对于 Objective-C 的 KVC, KVO 细节可参考 objc.io 的 Key-Value Coding and Observing 以及 Mattt 大神的 Key-Value Observing.

Objective-C 的这种 KVO 表面上对被观察对象没有任何影响(之所以说表面上, 是因为其背后是做了 isa-swizzling 的, 对原对象是有破坏的. 参见 Key-Value Observing Implementation Details), 而且同一对象的同一属性可以有多个观察者. Javascript 却没有如此完美的解决方案, 但是 Javascript 也可以实现类似的功能. 其原理是对原对象的要被监听的属性的setter方法做inject:

Key Value Observing
ObjC JS
// Add an observer for myObject's myValue
[myObject addObserver:myObserverObject 
forKeyPath:@"myValue"
options:0
context:nil];

// Set a value
[myObject setValue:@123 forKey:@"myValue"]

// When setting the new value, KVO calls myObserver's 
// observeValueForKeyPath:ofObject:change:context:
function callMeWhenMyValueChanged(newValue) {
  // Receiving the new value of myObject.myValue
}

// Setup a notification for when myObject.myValue changes
myObject.__defineSetter__('myValue', callMeWhenMyValueChanged)

// Set a value, this will trigger callMeWhenMyValueChanged
myObject['myValue'] = 123

MDN 上我们可以看到 __defineSetter__ (即Object.prototype.__defineSetter__()) 已经被弃用了, 但是为了展示方便, 上面的表格中还是使用了它, 后面我们会看到其ES 5的替代方案.

基本上我们遇到的比较新的 Javascript 框架中的诸如 “绑定”, “监听” 等都是这么实现的. 比如 Backbone 的模型变化事件; Ember 的控制器绑定Observable; 以及Angular和KnockoutJS.

下面代码是从上述框架中剥离的完整的属性变化监听代码:

function watch(target, prop, handler) {
  // if have already defined the accessors 
  if (Object.getOwnPropertyDescriptor) { // ECMAScript 5
    var propDesc = Object.getOwnPropertyDescriptor(target, prop);
    if (propDesc && propDesc.get) {
      return this;
    }
  } else if (Object.prototype.__lookupGetter__) { // legacy
    if (Object.prototype.__lookupGetter__.call(target, prop) != null) {
      return this;
    }
  }

  var oldval = target[prop],
    newval = oldval,
    self = this,
    getter = function () {
      return newval;
    },
    setter = function (val) {
      if (Object.prototype.toString.call(val) === '[object Array]') {
        val = _extendArray(val, handler, self);
      }
      oldval = newval;
      newval = val;
      handler.call(target, prop, oldval, val);
    };
  if (delete target[prop]) { // can't watch constants
    if (Object.defineProperty) { // ECMAScript 5
      Object.defineProperty(target, prop, {
        get: getter,
        set: setter,
        enumerable: false,
        configurable: true
      });
    } else if (Object.prototype.__defineGetter__ && Object.prototype.__defineSetter__) { // legacy
      Object.prototype.__defineGetter__.call(target, prop, getter);
      Object.prototype.__defineSetter__.call(target, prop, setter);
    }
  }
  return this;
};

function unwatch(target, prop) {
  var val = target[prop];
  delete target[prop]; // remove accessors
  target[prop] = val;
  return this;
};

// Allows operations performed on an array instance to trigger bindings
function _extendArray(arr, callback, motive) {
  if (arr.__wasExtended === true) return;

  function generateOverloadedFunction(target, methodName, self) {
    return function () {
      var oldValue = Array.prototype.concat.apply(target);
      var newValue = Array.prototype[methodName].apply(target, arguments);
      target.updated(oldValue, motive);
      return newValue;
    };
  }
  arr.updated = function (oldValue, self) {
    callback.call(this, 'items', oldValue, this, motive);
  };
  arr.concat = generateOverloadedFunction(arr, 'concat', motive);
  arr.join = generateOverloadedFunction(arr, 'join', motive);
  arr.pop = generateOverloadedFunction(arr, 'pop', motive);
  arr.push = generateOverloadedFunction(arr, 'push', motive);
  arr.reverse = generateOverloadedFunction(arr, 'reverse', motive);
  arr.shift = generateOverloadedFunction(arr, 'shift', motive);
  arr.slice = generateOverloadedFunction(arr, 'slice', motive);
  arr.sort = generateOverloadedFunction(arr, 'sort', motive);
  arr.splice = generateOverloadedFunction(arr, 'splice', motive);
  arr.unshift = generateOverloadedFunction(arr, 'unshift', motive);
  arr.__wasExtended = true;

  return arr;
}

原理前面已经提到, 是定义被监听属性的 setter 方法, 在 setter 方法中调用监听方法. 需要注意的是, 在 setter 方法中不能再对该属性进行写操作, 会死循环, 所以我们必须借助于另外一个变量, 这样的话就必须把 getter 也改写了.

现在你可以注册一个handler, 它绑定的属性一旦被更改(包括数组内部元素的改变), 该函数就会被调用:

var data = {};
var watcher = function(propertyName, oldValue, newValue){
  console.log(propertyName);
  console.log(oldValue);
  console.log(newValue);
};

watch(data, 'quantity', watcher);
watch(data, 'products', watcher);

现在改变 data 的quantity或者products属性, watcher 会被触发:

data.quantity = 2;
data.products.push('kindle');

console 会输出被改变的属性的名称, 改变之前的值和改变后的值.

这样, 我们实现了最基本的监听. 但这里有一个问题, 同一个属性不能被多次注册, 多次注册的话, 只有第一次注册的有效. 我们可以对 watch 函数作如下更改:

function watch(target, prop, handler) {
  ((target.__bindHandlers = target.__bindHandlers || {})[prop] = target.__bindHandlers[prop] || []).push(handler);
  // if have already defined the accessors 
  if (Object.getOwnPropertyDescriptor) { // ECMAScript 5
    var propDesc = Object.getOwnPropertyDescriptor(target, prop);
    if (propDesc && propDesc.get) {
      return this;
    }
  } else if (Object.prototype.__lookupGetter__) { // legacy
    if (Object.prototype.__lookupGetter__.call(target, prop) != null) {
      return this;
    }
  }

  var oldval = target[prop],
    newval = oldval,
    self = this,
    getter = function () {
      return newval;
    },
    setter = function (val) {
      if (Object.prototype.toString.call(val) === '[object Array]') {
        val = _extendArray(val, handler, self);
      }
      oldval = newval;
      newval = val;
      target.__bindHandlers[prop].forEach(function(handler) {
        handler.call(target, prop, oldval, val);
      });
    };
  if (delete target[prop]) { // can't watch constants
    if (Object.defineProperty) { // ECMAScript 5
      Object.defineProperty(target, prop, {
        get: getter,
        set: setter,
        enumerable: false,
        configurable: true
      });
    } else if (Object.prototype.__defineGetter__ && Object.prototype.__defineSetter__) { // legacy
      Object.prototype.__defineGetter__.call(target, prop, getter);
      Object.prototype.__defineSetter__.call(target, prop, setter);
    }
  }
  return this;
};

这样, 当属性被改变后, 所有注册的 handler 按照注册先后顺序被依次调用.

原文:
Javascript KVC/KVO and Variable Change Watchers

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值