diff --git a/src/binding/bindingAttributeSyntax.js b/src/binding/bindingAttributeSyntax.js index 8b89578e1..bc44f4c9e 100755 --- a/src/binding/bindingAttributeSyntax.js +++ b/src/binding/bindingAttributeSyntax.js @@ -6,22 +6,38 @@ return ko.bindingHandlers[bindingKey]; }; + // The ko.bindingContext constructor is only called directly to create the root context. For child + // contexts, use bindingContext.createChildContext or bindingContext.extend. ko.bindingContext = function(dataItemOrAccessor, parentContext, dataItemAlias, extendCallback) { + + // The binding context object includes static properties for the current, parent, and root view models. + // If a view model is actually stored in an observable, the corresponding binding context object, and + // any child contexts, must be updated when the view model is changed. function updateContext() { + // Most of the time, the context will directly get a view model object, but if a function is given, + // we call the function to retrieve the view model. If the function accesses any obsevables (or is + // itself an observable), the dependency is tracked, and those observables can later cause the binding + // context to be updated. var dataItem = isFunc ? dataItemOrAccessor() : dataItemOrAccessor; + if (parentContext) { - // Register a dependency on the parent context + // When a "parent" context is given, register a dependency on the parent context. Thus whenever the + // parent context is updated, this context will also be updated. if (parentContext._subscribable) parentContext._subscribable(); - // Inherit $root and any custom properties + + // Copy $root and any custom properties from the parent context ko.utils.extend(self, parentContext); - // Update our properties + + // Because the above copy overwrites our own properties, we need to reset them. + // During the first execution, "subscribable" isn't set, so don't bother doing the update then. if (subscribable) { self['$dataFn'] = self._subscribable = subscribable; } } else { self['$parents'] = []; self['$root'] = dataItem; + // Export 'ko' in the binding context so it will be available in bindings and templates // even if 'ko' isn't exported as a global, such as when using an AMD loader. // See https://github.com/SteveSanderson/knockout/issues/490 @@ -30,8 +46,13 @@ self['$data'] = dataItem; if (dataItemAlias) self[dataItemAlias] = dataItem; + + // The extendCallback function is provided when creating a child context or extending a context. + // It handles the specific actions needed to finish setting up the binding context. Actions in this + // function could also add dependencies to this binding context. if (extendCallback) extendCallback(self, parentContext, dataItem); + return self['$data']; } function disposeWhen() { @@ -43,10 +64,20 @@ nodes = [], subscribable = ko.dependentObservable(updateContext, null, { disposeWhen: disposeWhen }); + // At this point, the binding context has been initialized, and the "subscribable" computed observable is + // subscribed to any observables that were accessed in the process. If there is nothing to track, the + // computed will be inactive, and we can safely throw it away. If it's active, the computed is stored in + // the context object. if (subscribable.isActive()) { self['$dataFn'] = self._subscribable = subscribable; + // We need to be able to dispose of this computed observable when it's no longer needed. This would be + // easy if we had a single node to watch, but binding contexts can be used by many different nodes, and + // we cannot assume that those nodes have any relation to each other. So instead we track any node that + // the context is attached to, and dispose the computed when all of those nodes have been cleaned. + // Add properties to *subscribable* instead of *self* because any properties added to *self* may be overwritten on updates + subscribable._nodes = nodes; subscribable._addNode = function(node) { nodes.push(node); ko.utils.domNodeDisposal.addDisposeCallback(node, function(node) { @@ -56,24 +87,35 @@ self._subscribable = subscribable = undefined; } }); - // Make sure that our parent context is watching at least one node + // Make sure that the parent context is watching at least one node; otherwise the parent context might + // get disponsed right away. if (parentContext && parentContext._subscribable && !parentContext._subscribable._nodes.length) { parentContext._subscribable._addNode(node); } }; - subscribable._nodes = nodes; } else { self['$dataFn'] = function() { return self['$data']; } } } + + // Extend the binding context hierarchy with a new view model object. If the parent context is watching + // any obsevables, the new child context will automatically get a dependency on the parent context. + // But this does not mean that the $data value of the child context will also get updated. If the child + // view model also depends on the parent view model, you must provide a function that returns the correct + // view model on each update. ko.bindingContext.prototype['createChildContext'] = function (dataItemOrAccessor, dataItemAlias) { return new ko.bindingContext(dataItemOrAccessor, this, dataItemAlias, function(self, parentContext) { + // Extend the context hierarchy by setting the appropriate pointers self['$parentContext'] = parentContext; self['$parent'] = parentContext['$data']; self['$parents'] = (parentContext['$parents'] || []).slice(0); self['$parents'].unshift(self['$parent']); }); }; + + // Extend the binding context with new custom properties. This doesn't change the context hierarchy. + // Similarly to "child" contexts, provide a function here to make sure that the correct values are set + // when an observable view model is updated. ko.bindingContext.prototype['extend'] = function(properties) { return new ko.bindingContext(this['$dataFn'], this, null, function(self) { ko.utils.extend(self, typeof(properties) == "function" ? properties() : properties); @@ -218,6 +260,9 @@ var provider = ko.bindingProvider['instance'], getBindings = provider['getBindingAccessors'] || getBindingsAndMakeAccessors; + // When an obsevable view model is used, the binding context will expose an observable $dataFn value. A binding + // provider that supports observable view models should use bindingContext.$dataFn to access the view model, thus + // triggering a dependency here. var bindingsUpdater = ko.dependentObservable( function() { return (bindings = getBindings.call(provider, node, bindingContext)); @@ -231,6 +276,9 @@ var bindingHandlerThatControlsDescendantBindings; if (bindings) { + // Return the value accessor for a given binding. When bindings are static (won't be updated because of a binding + // context update), just return the value accessor from the binding. Otherwise, return a function that always gets + // the latest binding value and registers a dependency on the binding updater. var getValueAccessor = bindingsUpdater ? function(bindingKey) { return function() { diff --git a/src/binding/bindingProvider.js b/src/binding/bindingProvider.js index d20a2a42d..6f34ec8ef 100644 --- a/src/binding/bindingProvider.js +++ b/src/binding/bindingProvider.js @@ -59,6 +59,8 @@ // Build the source for a function that evaluates "expression" // For each scope variable, add an extra level of "with" nesting // Example result: with(sc1) { with(sc0) { return (expression) } } + // This binding evaluator function uses "$dataFn()" so that the bindings have a dependency + // on a binding context that's updatable. var rewrittenBindings = ko.expressionRewriting.preProcessBindings(bindingsString, options), functionBody = "with($context){with($dataFn()||{}){return{" + rewrittenBindings + "}}}"; return new Function("$context", "$element", functionBody);