• Jump To … +
    can-observation.js recorder-dependency-helpers.js temporarily-bind.js
  • ¶
    /* global require */
  • ¶

    can-observation

    var namespace = require('can-namespace');
    var canReflect = require('can-reflect');
    var queues = require("can-queues");
    var ObservationRecorder = require("can-observation-recorder");
    
    var canSymbol = require("can-symbol");
    var dev = require("can-log/dev/dev");
    var valueEventBindings = require("can-event-queue/value/value");
    
    var recorderHelpers = require("./recorder-dependency-helpers");
    var temporarilyBind = require("./temporarily-bind");
    
    var dispatchSymbol = canSymbol.for("can.dispatch");
    var getChangesSymbol = canSymbol.for("can.getChangesDependencyRecord");
    var getValueDependenciesSymbol = canSymbol.for("can.getValueDependencies");
  • ¶

    Observation constructor

    function Observation(func, context, options){
    	this.func = func;
    	this.context = context;
    	this.options = options || {priority: 0, isObservable: true};
  • ¶

    A flag if we are bound or not

    	this.bound = false;
  • ¶

    These properties will manage what our new and old dependencies are.

    	this.newDependencies = ObservationRecorder.makeDependenciesRecord();
    	this.oldDependencies = null;
  • ¶

    Make functions we need to pass around and maintain this.

    	var self = this;
    	this.onDependencyChange = function(newVal){
    		self.dependencyChange(this, newVal);
    	};
    	this.update = this.update.bind(this);
  • ¶

    Add debugging names. !steal-remove-start

    	this.onDependencyChange[getChangesSymbol] = function getChanges() {
    		return {
    			valueDependencies: new Set([self])
    		};
    	};
    	Object.defineProperty(this.onDependencyChange, "name", {
    		value: canReflect.getName(this) + ".onDependencyChange",
    	});
    	Object.defineProperty(this.update, "name", {
    		value: canReflect.getName(this) + ".update",
    	});
  • ¶

    !steal-remove-end

    }
  • ¶

    Observation prototype methods

  • ¶

    Mixin value event bindings. This is where the following are added:

    • .handlers which call onBound and onUnbound
    • .on / .off
    • can.onValue can.offValue
    • can.getWhatIChange
    valueEventBindings(Observation.prototype);
    
    canReflect.assign(Observation.prototype, {
  • ¶

    Starts observing changes and adds event listeners.

    	onBound: function(){
    		this.bound = true;
  • ¶

    Store the old dependencies

    		this.oldDependencies = this.newDependencies;
  • ¶

    Start recording dependencies.

    		ObservationRecorder.start();
  • ¶

    Call the observation’s function and update the new value.

    		this.value = this.func.call(this.context);
  • ¶

    Get the new dependencies.

    		this.newDependencies = ObservationRecorder.stop();
  • ¶

    Diff and update the bindings. On change, everything will call this.onDependencyChange, which calls this.dependencyChange.

    		recorderHelpers.updateObservations(this);
    	},
  • ¶

    This is called when any of the dependencies change. It queues up an update in the deriveQueue to be run after all source observables have had time to notify all observables that “derive” their value.

    	dependencyChange: function(context, args){
    		if(this.bound === true) {
  • ¶

    Update this observation after all notify tasks have been run.

    			queues.deriveQueue.enqueue(
    				this.update,
    				this,
    				[],
    				{
    					priority: this.options.priority
  • ¶

    !steal-remove-start

    					/* jshint laxcomma: true */
    					, log: [ canReflect.getName(this.update) ]
    					/* jshint laxcomma: false */
  • ¶

    !steal-remove-end

    				}
  • ¶

    !steal-remove-start

    				/* jshint laxcomma: true */
    				, [canReflect.getName(context), "changed"]
    				/* jshint laxcomma: false */
  • ¶

    !steal-remove-end

    			);
    		}
    	},
  • ¶

    Called to update its value as part of the derive queue.

    	update: function() {
    		if (this.bound === true) {
  • ¶

    Keep the old value.

    			var oldValue = this.value;
    			this.oldValue = null;
  • ¶

    Re-run this.func and update dependency bindings.

    			this.onBound();
  • ¶

    If our value changed, call the dispatch method provided by can-event-queue/value/value.

    			if (oldValue !== this.value) {
    				this[dispatchSymbol](this.value, oldValue);
    			}
    		}
    	},
  • ¶

    Called when nothing is bound to this observation. Removes all event listeners on all dependency observables.

    	onUnbound: function(){
    		this.bound = false;
    		recorderHelpers.stopObserving(this.newDependencies, this.onDependencyChange);
  • ¶

    Setup newDependencies in case someone binds again to this observable.

    		this.newDependencies = ObservationRecorder.makeDependenciesRecorder();
    	},
  • ¶

    Reads the value of the observation.

    	get: function(){
  • ¶

    If an external observation is tracking observables and this compute can be listened to by “function” based computes ….

    		if( this.options.isObservable && ObservationRecorder.isRecording() ) {
  • ¶

    … tell the tracking compute to listen to change on this observation.

    			ObservationRecorder.add(this);
  • ¶

    … if we are not bound, we should bind so that we don’t have to re-read to get the value of this observation.

    			if (!this.bound) {
    				Observation.temporarilyBind(this);
    			}
    
    		}
    
    
    		if(this.bound === true ) {
  • ¶

    It’s possible that a child dependency of this observable might be queued to change. Check all child dependencies and make sure they are up-to-date by possibly running what they have registered in the derive queue.

    			if(queues.deriveQueue.tasksRemainingCount() > 0) {
    				Observation.updateChildrenAndSelf(this);
    			}
    
    			return this.value;
    		} else {
  • ¶

    If we are not bound, just call the function.

    			return this.func.call(this.context);
    		}
    	},
    
    	hasDependencies: function(){
    		var newDependencies = this.newDependencies;
    		return this.bound ?
    			(newDependencies.valueDependencies.size + newDependencies.keyDependencies.size) > 0  :
    			undefined;
    	},
    	log: function() {
  • ¶

    !steal-remove-start

    		var quoteString = function quoteString(x) {
    			return typeof x === "string" ? JSON.stringify(x) : x;
    		};
    		this._log = function(previous, current) {
    			dev.log(
    				canReflect.getName(this),
    				"\n is  ", quoteString(current),
    				"\n was ", quoteString(previous)
    			);
    		};
  • ¶

    !steal-remove-end

    	}
    });
    
    canReflect.assignSymbols(Observation.prototype, {
    	"can.getValue": Observation.prototype.get,
    	"can.isValueLike": true,
    	"can.isMapLike": false,
    	"can.isListLike": false,
    	"can.valueHasDependencies": Observation.prototype.hasDependencies,
    	"can.getValueDependencies": function(){
    		if (this.bound === true) {
  • ¶

    Only provide keyDependencies and valueDependencies properties if there’s actually something there.

    			var deps = this.newDependencies,
    				result = {};
    
    			if (deps.keyDependencies.size) {
    				result.keyDependencies = deps.keyDependencies;
    			}
    
    			if (deps.valueDependencies.size) {
    				result.valueDependencies = deps.valueDependencies;
    			}
    
    			return result;
    		}
    		return undefined;
    	},
    	"can.getPriority": function(){
    		return this.options.priority;
    	},
    	"can.setPriority": function(priority){
    		this.options.priority = priority;
    	},
  • ¶

    !steal-remove-start

    	"can.getName": function() {
    		return canReflect.getName(this.constructor) + "<" + canReflect.getName(this.func) + ">";
    	}
  • ¶

    !steal-remove-end

    });
  • ¶

    Observation.updateChildrenAndSelf

    This recursively checks if an observation’s dependencies might be in the derive queue. If it is, we need to update that value so the reading of this value will be correct. This can happen if an observation suddenly switches to depending on something that has higher priority than itself. We need to make sure that value is completely updated.

    Observation.updateChildrenAndSelf = function(observation){
  • ¶

    If the observable has an update method and it’s enqueued, flush that task immediately so the value is right.

    NOTE: This only works for Observation right now. We need a way of knowing how to find what an observable might have in the deriveQueue.

    	if(observation.update !== undefined && queues.deriveQueue.isEnqueued( observation.update ) === true) {
  • ¶

    TODO: In the future, we should be able to send log information to explain why this needed to be updated.

    		queues.deriveQueue.flushQueuedTask(observation.update);
    		return true;
    	}
  • ¶

    If we can get dependency values from this observable …

    	if(observation[getValueDependenciesSymbol]) {
  • ¶

    … Loop through each dependency and see if any of them (or their children) needed an update.

    		var childHasChanged = false;
    		var valueDependencies = observation[getValueDependenciesSymbol]().valueDependencies || [];
    		valueDependencies.forEach(function(observable){
    			if( Observation.updateChildrenAndSelf( observable ) === true) {
    				childHasChanged = true;
    			}
    		});
    		return childHasChanged;
    	} else {
    		return false;
    	}
    };
  • ¶

    Legacy Stuff

    Warn when ObservationRecorder methods are called on Observation.

    var alias = {addAll: "addMany"};
    ["add","addAll","ignore","trap","trapsCount","isRecording"].forEach(function(methodName){
    	Observation[methodName] = function(){
    		var name = alias[methodName] ? alias[methodName] : methodName;
    		console.warn("can-observation: Call "+name+"() on can-observation-recorder.");
    		return ObservationRecorder[name].apply(this, arguments);
    	};
    });
    Observation.prototype.start = function(){
    	console.warn("can-observation: Use .on and .off to bind.");
    	return this.onBound();
    };
    Observation.prototype.stop = function(){
    	console.warn("can-observation: Use .on and .off to bind.");
    	return this.onUnbound();
    };
  • ¶

    temporarilyBind

    Will bind an observable value temporarily. This should be part of queues probably.

    Observation.temporarilyBind = temporarilyBind;
    
    
    module.exports = namespace.Observation = Observation;