UNPKG

can

Version:

MIT-licensed, client-side, JavaScript framework that makes building rich web applications easy.

342 lines (309 loc) 9.19 kB
// # can/compute/get_value_and_bind // // This module: // // Exports a function that calls an arbitrary function and binds to any observables that // function reads. When any of those observables change, a callback function is called. // // And ... // // Adds two main methods to can: // // - can.__observe - All other observes call this method to be visible to computed functions. // - can.__notObserve - Returns a function that can not be observed. steal("can/util", function(can){ function ObservedInfo(func, context, compute){ this.newObserved = {}; this.oldObserved = null; this.func = func; this.context = context; this.compute = compute; this.onDependencyChange = can.proxy(this.onDependencyChange, this); this.depth = null; this.childDepths = {}; this.ignore = 0; this.inBatch = false; this.ready = false; compute.observedInfo = this; this.setReady = can.proxy(this._setReady, this); } can.simpleExtend(ObservedInfo.prototype,{ getPrimaryDepth: function() { return this.compute._primaryDepth; }, _setReady: function(){ this.ready = true; }, getDepth: function(){ if(this.depth !== null) { return this.depth; } else { return (this.depth = this._getDepth()); } }, _getDepth: function(){ var max = 0, childDepths = this.childDepths; for(var cid in childDepths) { if(childDepths[cid] > max) { max = childDepths[cid]; } } return max + 1; }, addEdge: function(objEv){ objEv.obj.bind(objEv.event, this.onDependencyChange); if(objEv.obj.observedInfo) { this.childDepths[objEv.obj._cid] = objEv.obj.observedInfo.getDepth(); this.depth = null; } }, removeEdge: function(objEv){ objEv.obj.unbind(objEv.event, this.onDependencyChange); if(objEv.obj.observedInfo) { delete this.childDepths[objEv.obj._cid]; this.depth = null; } }, dependencyChange: function(ev){ if(this.bound && this.ready) { if(ev.batchNum !== undefined) { // Only need to register once per batchNum if(ev.batchNum !== this.batchNum) { ObservedInfo.registerUpdate(this); this.batchNum = ev.batchNum; } } else { this.updateCompute(ev.batchNum); } } }, onDependencyChange: function(ev, newVal, oldVal){ this.dependencyChange(ev, newVal, oldVal); }, updateCompute: function(batchNum){ // It's possible this became unbound since it was registered to update // Only actually update if something didn't come in and unbind it. (#2188). if(this.bound) { // Keep the old value. var oldValue = this.value; // Get the new value and register this event handler to any new observables. this.getValueAndBind(); // Update the compute with the new value. this.compute.updater(this.value, oldValue, batchNum); } }, // ## getValueAndBind // Calls `func` with "this" as `context` and binds to any observables that // `func` reads. When any of those observables change, `onchanged` is called. // `oldObservedInfo` is A map of observable / event pairs this function used to be listening to. // Returns the `newInfo` set of listeners and the value `func` returned. getValueAndBind: function() { this.bound = true; this.oldObserved = this.newObserved || {}; this.ignore = 0; this.newObserved = {}; this.ready = false; // Add this function call's observedInfo to the stack, // runs the function, pops off the observedInfo, and returns it. observedInfoStack.push(this); this.value = this.func.call(this.context); observedInfoStack.pop(); this.updateBindings(); can.batch.afterPreviousEvents(this.setReady); }, // ### updateBindings // Unbinds everything in `oldObserved`. updateBindings: function(){ var newObserved = this.newObserved, oldObserved = this.oldObserved, name, obEv; for (name in newObserved) { obEv = newObserved[name]; if(!oldObserved[name]) { this.addEdge(obEv); } else { oldObserved[name] = null; } } for (name in oldObserved) { obEv = oldObserved[name]; if(obEv) { this.removeEdge(obEv); } } }, teardown: function(){ // track this because events can be in the queue. this.bound = false; for (var name in this.newObserved) { var ob = this.newObserved[name]; this.removeEdge(ob); } this.newObserved = {}; } }); var updateOrder = [], curPrimaryDepth = Infinity, maxPrimaryDepth = 0, currentBatchNum; // could get a registerUpdate from a 5 while a 1 is going on because the 5 listens to the 1 ObservedInfo.registerUpdate = function(observeInfo, batchNum){ var depth = observeInfo.getDepth()-1; var primaryDepth = observeInfo.getPrimaryDepth(); curPrimaryDepth = Math.min(primaryDepth, curPrimaryDepth); maxPrimaryDepth = Math.max(primaryDepth, maxPrimaryDepth); var primary = updateOrder[primaryDepth] || (updateOrder[primaryDepth] = { observeInfos: [], current: Infinity, max: 0 }); var objs = primary.observeInfos[depth] || (primary.observeInfos[depth] = []); objs.push(observeInfo); primary.current = Math.min(depth, primary.current); primary.max = Math.max(depth, primary.max); }; /* * update all computes to the specified place. */ ObservedInfo.updateUntil = function(primaryDepth, depth){ var cur; while(true) { if(curPrimaryDepth <= maxPrimaryDepth && curPrimaryDepth <= primaryDepth) { var primary = updateOrder[curPrimaryDepth]; if(primary && primary.current <= primary.max) { if(primary.current > depth) { return; } var last = primary.observeInfos[primary.current]; if(last && (cur = last.pop())) { cur.updateCompute(currentBatchNum); } else { primary.current++; } } else { curPrimaryDepth++; } } else { return; } } }; ObservedInfo.batchEnd = function(batchNum){ var cur; currentBatchNum = batchNum; while(true) { if(curPrimaryDepth <= maxPrimaryDepth) { var primary = updateOrder[curPrimaryDepth]; if(primary && primary.current <= primary.max) { var last = primary.observeInfos[primary.current]; if(last && (cur = last.pop())) { cur.updateCompute(batchNum); } else { primary.current++; } } else { curPrimaryDepth++; } } else { updateOrder = []; curPrimaryDepth = Infinity; maxPrimaryDepth = 0; return; } } }; // ### observedInfoStack // // This is the stack of all `observedInfo` objects that are the result of // recursive `getValueAndBind` calls. // `getValueAndBind` can indirectly call itself anytime a compute reads another // compute. // // An `observedInfo` entry looks like: // // { // observed: { // "map1|first": {obj: map, event: "first"}, // "map1|last" : {obj: map, event: "last"} // }, // names: "map1|firstmap1|last" // } // // Where: // - `observed` is a map of `"cid|event"` to the observable and event. // We use keys like `"cid|event"` to quickly identify if we have already observed this observable. // - `names` is all the keys so we can quickly tell if two observedInfo objects are the same. var observedInfoStack = []; // ## can.__observe // Indicates that an observable is being read. // Updates the top of the stack with the observable being read. can.__observe = function (obj, event) { var top = observedInfoStack[observedInfoStack.length-1]; if (top && !top.ignore) { var evStr = event + "", name = obj._cid + '|' + evStr; if(top.traps) { top.traps.push({obj: obj, event: evStr, name: name}); } else if(!top.newObserved[name]) { top.newObserved[name] = { obj: obj, event: evStr }; } } }; can.__reading = can.__observe; can.__trapObserves = function(){ if (observedInfoStack.length) { var top = observedInfoStack[observedInfoStack.length-1]; var traps = top.traps = []; return function(){ top.traps = null; return traps; }; } else { return function(){return [];}; } }; can.__observes = function(observes){ // a bit more optimized so we don't have to repeat everything in can.__observe var top = observedInfoStack[observedInfoStack.length-1]; if (top) { for(var i =0, len = observes.length; i < len; i++) { var trap = observes[i], name = trap.name; if(!top.newObserved[name]) { top.newObserved[name] = trap; } } } }; // ### can.__isRecordingObserves // Returns if some function is in the process of recording observes. can.__isRecordingObserves = function(){ var len = observedInfoStack.length, last = observedInfoStack[len-1]; return len && (last.ignore === 0) && last; }; // ### can.__notObserve // Protects a function from being observed. can.__notObserve = function(fn){ return function(){ if (observedInfoStack.length) { var top = observedInfoStack[observedInfoStack.length-1]; top.ignore++; var res = fn.apply(this, arguments); top.ignore--; return res; } else { return fn.apply(this, arguments); } }; }; can.batch._onDispatchedEvents = ObservedInfo.batchEnd; return ObservedInfo; });