Skip to content

Instantly share code, notes, and snippets.

@srikumarks
Created December 14, 2011 06:20
Show Gist options
  • Save srikumarks/1475489 to your computer and use it in GitHub Desktop.
Save srikumarks/1475489 to your computer and use it in GitHub Desktop.
A utility to watch for changes to the value addressed by an object and a key.
var watch = (function () {
// A task queue for calling tasks asynchronously.
// Note that one task queue processes all the notification
// callbacks for all installed watchers. This allows
// the use of lots of watchers without incurring a
// corresponding increase in setTimeouts. If we
// did each notification in its own timer callback,
// then we might incur unwanted delays between
// chained callbacks, which is not desirable if
// you're doing animations, for example.
//
// asyncMethod(task) is expected to call task()
// at a later point in time. Possibilites are to use
// a setTimeout based function or requestAnimationFrame.
function MkTaskQueue(asyncMethod) {
var head = null, tail = null;
function doAllTasks() {
// Process notifications from the tail so that
// if the callback ends up triggering new notifications,
// they only change the head and we'll finish processing
// them as well right in this loop.
var node = tail;
while (node) {
node.task();
node = node.prev;
}
// The whole queue is empty now.
head = tail = null;
}
function enqueue(task) {
var node = {task: task, next: head, prev: null};
if (head) {
head.prev = node;
}
head = node;
if (!tail) {
tail = head;
// Whenever an element is added to an empty queue,
// fire off a processing task. That task will
// empty the queue and the tail value will be
// returned to null.
asyncMethod(doAllTasks);
}
}
return { enqueue: enqueue };
}
// A doubly linked list used as a queue for callbacks.
var taskQueue = MkTaskQueue(function (task) { setTimeout(task, 0); });
var animQueue = MkTaskQueue(window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame);
// Usage:
// watch(obj, key, function (v) {...});
//
// @param obj is an object that's the target of the watch
// @param key is the key of the object to watch
// @param onchange is a function (value) {... return done;}
// which will be called with the new value whenever the
// value of the given key in the given object *changes*.
// If the return value of onchange call is true, then
// it means the watcher is done and doesn't need to be
// called again and is removed from the list of callbacks.
// Otherwise the watcher is kept in the list of callbacks.
// Note that if you don't have a return statement in the
// callback, that is equivalent to "return false;".
//
// Implementation:
// Watching is implemented by replacing the key of the object
// with a getter and setter. The setter will change the value
// as expected, but will also trigger an asynchronous notification
// task. You can install multiple watchers on the same obj/key
// pair.
function watch(obj, key, onchange) {
var getter, setter, value, node;
getter = obj.__lookupGetter__(key);
if (getter) {
setter = obj.__lookupSetter__(key);
// We use the presence of the "notificationTargets" linked list as an
// indicator that the setter has been created by watch().
// Note that we only touch the 'head' of the list and not the
// tail. This is to allow watchers to be added while callback
// processing is going on.
if (setter && setter.notificationTargets) {
node = {task: onchange, next: setter.notificationTargets.head, prev: null};
if (node.next) {
node.next.prev = node;
}
setter.notificationTargets.head = node;
return obj;
}
throw new Error('Invalid key for observation');
} else {
// Install getter and setter for this obj/key pair.
// Save the current value
value = obj[key];
// The getter is easy. "value" is closed over by
// both the getter and setter and serves as the
// communication channel between them.
getter = function () { return value; };
// The notifier invokes all the onchange handlers
// assigned to watch this particular key. Note that
// this is compatible with the case where a new
// watcher is added for this same key on this same
// object *within* a callback. Granted, that is
// a weird thing to do, but the design is safe
// in that weird scenario as well.
function notifier() {
setter.needsNotification = true;
var node = setter.notificationTargets.tail, newHead = node, newTail = node;
while (node) {
if (node.task(value)) {
// Remove the callback. It is done.
if (node.prev) {
node.prev.next = node.next;
}
if (node.next) {
node.next.prev = node.prev;
}
newHead = node.next;
if (newTail === node) {
// The current tail becomes invalid.
newTail = node.prev;
}
node = node.prev;
} else {
// The callback wants to live on.
newHead = node;
node = node.prev;
}
}
setter.notificationTargets.head = newHead;
setter.notificationTargets.tail = newTail;
}
// The setter works by asynchronously triggering the
// notification callbacks. If it synchronously triggered
// it, JS could end up with too deep a stack and blow it.
setter = function (newValue) {
if (newValue !== value) {
value = newValue;
// Prevent multiple notification calls in the same
// VM clock tick. If the timeout hasn't fired
// between two value assignments, then a new
// notification is not triggered.
if (setter.needsNotification) {
setter.needsNotification = false;
taskQueue.enqueue(notifier);
}
}
return newValue;
};
// Initialize the targets array.
setter.notificationTargets = {head: null, tail: null};
setter.notificationTargets.tail = setter.notificationTargets.head = {task: onchange, next: null, prev: null};
setter.needsNotification = true;
obj.__defineGetter__(key, getter);
obj.__defineSetter__(key, setter);
return obj;
}
}
return watch;
}());
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment