KVC(Key Value Coding) and KVO(Key Value Observing) are terms in Objective-C (Swift is also supported, of course). To put it bluntly, KVC allows you to operate your object like NSDictionary; KVO allows you to monitor changes in the attributes of your object.
The so-called KVC is actually more intuitive in Javascript. Here are ObjC's KVC grammar and similar JS writing:
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'] = 123For Objective-C, KVC is the foundation of KVO.
The most common way to use KVO in Objective-C is to use addObserver: for KeyPath: options: context: to add an observer to an object's key path. Then the observer implements observeValue ForKeyPath: of Object: change: context: to respond to changes in the attributes of the object being observed. Finally, don't forget to use removeObserver: for K. EyPath: Delete the observer.
This article focuses on Javascript attributes change monitoring. For KVC of Objective-C, KVO details can be referred to objc.io's Key-Value Coding and Observing And Mattt's Key-Value Observing.
The KVO of Objective-C has no effect on the observed object (on the surface, it is isa-swizzling behind it, which is destructive to the original object). Key-Value Observing Implementation Details Javascript does not have such a perfect solution, but Javascript can also achieve similar functions. Its principle is to inject the setter method of the attributes of the original object to be monitored:
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'] = 123from MDN We can see that defineSetter _ (that is, Object. prototype. defineSetter ()) has been discarded, but for display convenience, it is still used in the table above, and we will see an alternative to its ES 5 later.
Basically, the newer Javascript frameworks we encounter such as "binding" and "listening" are all implemented in this way. For example, Backbone's Model change events Ember's Controller Binding and Observable And Angular and Knockout JS.
The following code is a complete property change monitoring code stripped from the above framework:
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; }
As mentioned earlier, it's the setter method that defines the listened property and calls the listened method in the setter method. It's important to note that in the setter method, the property can't be written any more, and the loop will be dead. So we have to resort to another variable, so we have to rewrite the getter as well.
Now you can register a handler, and once the properties bound to it are changed (including changes in the elements inside the array), the function will be called:
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);
Now change the data quantity or products attribute, and the watcher will be triggered:
data.quantity = 2; data.products.push('kindle');
The console outputs the name of the changed attribute, the previous value and the changed value.
In this way, we have achieved the most basic monitoring. But there is a problem here. The same attribute can not be registered many times. If registered many times, only the first registration is valid. We can make the following changes to the watch function:
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; };
In this way, when the attribute is changed, all registered handler s are invoked sequentially according to the order of registration.
Original:
Javascript KVC/KVO and Variable Change Watchers