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 按照注册先后顺序被依次调用.