什么是依赖
KnockoutJS是一个基于MVVM的实现,通过view和view model的双向绑定实现数据和UI的动态更新,用的是经典的观察者模式(observable)。除了简单的单个UI元素和单个observable的绑定,KnockoutJS支持在view model中添加一些复杂的observable对象(后面统一称作computed),使得他们可以监听其他observable的变化并且直接和UI进行绑定。这种复杂的computed和计算出它的值所依赖observable之间的关系就是我们要讨论的依赖关系。computed具有非常多的应用场景,例如经典的表单验证场景。表单UI有多个区域需要用户输入有效的值,最下方有一个提交按钮,只有当所有必须的区域都有了有效值的情况下才会被激活。这个时候我们就可以给每个输入区域绑定一个observable,给提交按钮绑定一个computed,监听输入区域的observable的值来决定是否激活这个按钮。
依赖检测
观察者模式中的一个核心操作就是建立observable对象之间的依赖关系。一种显而易见的实现方式就是让程序员手动定义observable之间的依赖关系。例如如下代码
var dependentOb1 = new Observable(...);
var dependentOb2 = new Observable(...);
...
var computed = new Computed([dependentOb1, dependentOb2], function() {
return dependentOb1() + dependentOb2();
});
这里我们定义了两个observable和一个依赖于这两个observable的computed对象,同时在构造函数中显示地声明了这个computed对象依赖的observable。因为有了这两个observable对象,我们就可以在构造函数中建立绑定关系,使得dependentOb1和dependentOb2在改变的时候主动通知computed改变它的值。
这种实现方法的一个缺点就是会导致代码非常繁琐,每次定义computed对象都要手动声明所有它的依赖关系。而且理论上所有真实存在的依赖关系只取决于computed的求值函数,手动声明的依赖关系和真实存在的依赖关系之间存在一个需要程序员自己确保的约束,增加了代码出现不必要错误的可能性。knockoutJS使用了一种非常巧妙的方法通过求职函数动态地发现并注册依赖关系,省去了手动声明依赖关系这一步骤。
依赖检测的原理总体上就是在构造computed对象的时候运行一次特殊的求值,使得在求值过程中所有被引用的observable对象和computed对象将自己添加到被构造的computed对象的依赖列表中。这样在大部分情况下第一次求值结束的时候被构造的computed对象就能检测到所有它依赖的observable对象,从而实现数值的动态更新。
控制依赖检测的通用代码在depencencyDetection.js里,
ko.computedContext = ko.dependencyDetection = (function () {
var outerFrames = [],
currentFrame,
lastId = 0;
// Return a unique ID that can be assigned to an observable for dependency tracking.
// Theoretically, you could eventually overflow the number storage size, resulting
// in duplicate IDs. But in JavaScript, the largest exact integral value is 2^53
// or 9,007,199,254,740,992. If you created 1,000,000 IDs per second, it would
// take over 285 years to reach that number.
// Reference http://blog.vjeux.com/2010/javascript/javascript-max_int-number-limits.html
function getId() {
return ++lastId;
}
function begin(options) {
outerFrames.push(currentFrame);
currentFrame = options;
}
function end() {
currentFrame = outerFrames.pop();
}
return {
begin: begin,
end: end,
registerDependency: function (subscribable) {
if (currentFrame) {
if (!ko.isSubscribable(subscribable))
throw new Error("Only subscribable things can act as dependencies");
currentFrame.callback.call(currentFrame.callbackTarget, subscribable, subscribable._id || (subscribable._id = getId()));
}
},
ignore: function (callback, callbackTarget, callbackArgs) {
try {
begin();
return callback.apply(callbackTarget, callbackArgs || []);
} finally {
end();
}
},
getDependenciesCount: function () {
if (currentFrame)
return currentFrame.computed.getDependenciesCount();
},
getDependencies: function () {
if (currentFrame)
return currentFrame.computed.getDependencies();
},
isInitial: function() {
if (currentFrame)
return currentFrame.isInitial;
},
computed: function() {
if (currentFrame)
return currentFrame.computed;
}
};
})();
这里我们对currentFrame对象展开细讲,在dependentObservable.js line可以看到currentFrame是一个这样的对象
ko.dependencyDetection.begin({
callbackTarget: dependencyDetectionContext,
callback: computedBeginDependencyDetectionCallback,
computed: computedObservable,
isInitial: isInitial
});
这里computed很显然就是这个被构造的computed对象,isInitial涉及到覆写computed的高级用法,这里不展开细讲。我们看callbackTarget和callback。
// callbackTarget
dependencyDetectionContext = {
computedObservable: computedObservable,
disposalCandidates: state.dependencyTracking,
disposalCount: state.dependenciesCount
};
...
// callback
// This function gets called each time a dependency is detected while evaluating a computed.
// It's factored out as a shared function to avoid creating unnecessary function instances during evaluation.
function computedBeginDependencyDetectionCallback(subscribable, id) {
// this points to the dependencyDetectionContext object above
var computedObservable = this.computedObservable,
state = computedObservable[computedState];
if (!state.isDisposed) {
if (this.disposalCount && this.disposalCandidates[id]) {
// Don't want to dispose this subscription, as it's still being used
computedObservable.addDependencyTracking(id, subscribable, this.disposalCandidates[id]);
this.disposalCandidates[id] = null; // No need to actually delete the property - disposalCandidates is a transient object anyway
--this.disposalCount;
} else if (!state.dependencyTracking[id]) {
// Brand new subscription - add it
computedObservable.addDependencyTracking(id, subscribable, state.isSleeping ? { _target: subscribable } : computedObservable.subscribeToDependency(subscribable));
}
// If the observable we've accessed has a pending notification, ensure we get notified of the actual final value (bypass equality checks)
if (subscribable._notificationIsPending) {
subscribable._notifyNextChangeIfValueIsDifferent();
}
}
}
注意dependencyDetection.js对函数的this应用进行了一些操作。筛去其他无关的代码后,行间的注释已经可以非常清晰地解释代码地内容了。addDependencyTracking()内的操作就是标准观察者模式建立订阅关系的操作,并无特殊之处,有兴趣的可以自行阅读相关代码。