Created
December 14, 2011 06:20
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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