• Jump To … +
    compute.js get_value_and_bind.js proto_compute.js read.js
  • ¶

    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;
    
    });