好久没更一些有质量的博文,而这段时间我想写一些有意义的东西。希望能对刚接触前段MVVM框架的朋友有一些帮助。哪怕是一些我就满足了。同是我希望本文章能帮助大家学会一些分析源码的方法,在这个开源的时代做一名优秀的程序员。
KnockOut.js可以说是MvvM的鼻祖了吧,关于他的使用我就不介绍了双向绑定。有兴趣的朋友们可以去看看博客园汤姆大叔的博文。
http://www.cnblogs.com/TomXu/archive/2011/11/21/2257154.html
关于源码分析如果真的拿knockout.js那个压缩版分析是完全没法分析的因为他是被压缩了 变量全部变成了a,b,c,d
所以要分析的话的。去github下载knockout的源码包。地址是http://github.com/SteveSanderson/knockout github上的地址把整个项目打散了 并通过业务逻辑分离到了多个js文件中 对分析源码很帮助。用vs加载项目如图。这里由于我是.net程序员所以我选择用vs分析 其他语言的程序员 也可以用自己熟悉的ide分析原理其实都是一样的。如图
下面我介绍一下这第一demo分析源码最好的方式就是用一个例子去调通它。这里我们将spec文件夹里的runner.html复制一份起名为demo.html然后修改其源代码保持引用不变如下。
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<!-- jasmine -->
<link rel="stylesheet" type="text/css" href="lib/jasmine-1.2.0/jasmine.css" />
<script type="text/javascript" src="lib/jasmine-1.2.0/jasmine.js"></script>
<script type="text/javascript" src="lib/jasmine-1.2.0/jasmine-html.js"></script>
<script type="text/javascript" src="lib/jasmine-1.2.0/jasmine-tap.js"></script>
<!-- our jasmine extensions -->
<link rel="stylesheet" type="text/css" href="lib/jasmine.extensions.css" />
<script type="text/javascript" src="lib/jasmine.extensions.js"></script>
<!-- knockout and other dependencies -->
<script type="text/javascript" src="lib/loadDependencies.js"></script>
<!-- specs -->
<script type="text/javascript" src="arrayEditDetectionBehaviors.js"></script>
<script type="text/javascript" src="arrayToDomEditDetectionBehaviors.js"></script>
<script type="text/javascript" src="asyncBehaviors.js"></script>
<script type="text/javascript" src="asyncBindingBehaviors.js"></script>
<script type="text/javascript" src="memoizationBehaviors.js"></script>
<script type="text/javascript" src="subscribableBehaviors.js"></script>
<script type="text/javascript" src="observableBehaviors.js"></script>
<script type="text/javascript" src="observableArrayBehaviors.js"></script>
<script type="text/javascript" src="observableArrayChangeTrackingBehaviors.js"></script>
<script type="text/javascript" src="dependentObservableBehaviors.js"></script>
<script type="text/javascript" src="dependentObservableDomBehaviors.js"></script>
<script type="text/javascript" src="pureComputedBehaviors.js"></script>
<script type="text/javascript" src="extenderBehaviors.js"></script>
<script type="text/javascript" src="domNodeDisposalBehaviors.js"></script>
<script type="text/javascript" src="mappingHelperBehaviors.js"></script>
<script type="text/javascript" src="expressionRewritingBehaviors.js"></script>
<script type="text/javascript" src="bindingPreprocessingBehaviors.js"></script>
<script type="text/javascript" src="nodePreprocessingBehaviors.js"></script>
<script type="text/javascript" src="bindingAttributeBehaviors.js"></script>
<script type="text/javascript" src="bindingDependencyBehaviors.js"></script>
<script type="text/javascript" src="templatingBehaviors.js"></script>
<script type="text/javascript" src="jsonPostingBehaviors.js"></script>
<script type="text/javascript" src="nativeTemplateEngineBehaviors.js"></script>
<script type="text/javascript" src="taskBehaviors.js"></script>
<script type="text/javascript" src="utilsBehaviors.js"></script>
<script type="text/javascript" src="utilsDomBehaviors.js"></script>
<script type="text/javascript" src="onErrorBehaviors.js"></script>
<script type="text/javascript" src="components/loaderRegistryBehaviors.js"></script>
<script type="text/javascript" src="components/defaultLoaderBehaviors.js"></script>
<script type="text/javascript" src="components/componentBindingBehaviors.js"></script>
<script type="text/javascript" src="components/customElementBehaviors.js"></script>
<!-- Default bindings -->
<script type="text/javascript" src="defaultBindings/attrBehaviors.js"></script>
<script type="text/javascript" src="defaultBindings/checkedBehaviors.js"></script>
<script type="text/javascript" src="defaultBindings/clickBehaviors.js"></script>
<script type="text/javascript" src="defaultBindings/cssBehaviors.js"></script>
<script type="text/javascript" src="defaultBindings/enableDisableBehaviors.js"></script>
<script type="text/javascript" src="defaultBindings/eventBehaviors.js"></script>
<script type="text/javascript" src="defaultBindings/foreachBehaviors.js"></script>
<script type="text/javascript" src="defaultBindings/hasfocusBehaviors.js"></script>
<script type="text/javascript" src="defaultBindings/htmlBehaviors.js"></script>
<script type="text/javascript" src="defaultBindings/ifBehaviors.js"></script>
<script type="text/javascript" src="defaultBindings/ifnotBehaviors.js"></script>
<script type="text/javascript" src="defaultBindings/optionsBehaviors.js"></script>
<script type="text/javascript" src="defaultBindings/selectedOptionsBehaviors.js"></script>
<script type="text/javascript" src="defaultBindings/styleBehaviors.js"></script>
<script type="text/javascript" src="defaultBindings/submitBehaviors.js"></script>
<script type="text/javascript" src="defaultBindings/textBehaviors.js"></script>
<script type="text/javascript" src="defaultBindings/textInputBehaviors.js"></script>
<script type="text/javascript" src="defaultBindings/uniqueNameBehaviors.js"></script>
<script type="text/javascript" src="defaultBindings/valueBehaviors.js"></script>
<script type="text/javascript" src="defaultBindings/visibleBehavko.dependentObservable iors.js"></script>
<script type="text/javascript" src="defaultBindings/withBehaviors.js"></script>
<script type="text/javascript" src="crossWindowBehaviors.js"></script>
<script type="text/javascript">
window.onload = function(){
var viewModel = {
firstName: ko.observable("Bill"),
lastName: ko.observable("Gates")
};
viewModel.fullName = ko.dependentObservable(function () {
return this.firstName() + " " + this.lastName();
}, viewModel);
ko.applyBindings(viewModel);
}
</script>
</head>
<body>
<div id="liveExample">
<p>First name:<span data-bind="attr:{'data-options':firstName}"></span>
</p>
<input data-bind="value:lastName" />
<p>Last name:<span data-bind="text:lastName"></span>
</p>
<p>Full name:<span data-bind="text:fullName"></span>
</p>
</div>
</body>
</html>
这样的话效果就跟引用的knouck.js一样了。而为了节约时间我从ko.applyBindings(viewModel);这句话分析起,因为上面的observable和dependentObservable都是为了这句话服务的。当然如果你是才开始研究一个框架也是无可厚非的但最终你会发现。
这里我想提供2种找函数的方法,比如这里我们如何知道ko.applyBindings具体执行的那个函数。2种方式一种是ctrl+f搜索如图
因为json对象定义属性无非就2种方式 当然有函数式导入 但常用的就是这种或者是{xx:xx,xx:xx}这种 可以尝试这样搜索一下如果搜不出来 可以去通过浏览器调试断点加F11观察其具体执行到哪里。
这里ko.applyBindings执行的方法如下代码
ko.applyBindings = function (viewModelOrBindingContext, rootNode) {
// If jQuery is loaded after Knockout, we won't initially have access to it. So save it here.
if (!jQueryInstance && window['jQuery']) {
jQueryInstance = window['jQuery'];
}
if (rootNode && (rootNode.nodeType !== 1) && (rootNode.nodeType !== 8))
throw new Error("ko.applyBindings: first parameter should be your view model; second parameter should be a DOM node");
rootNode = rootNode || window.document.body; // Make "rootNode" parameter optional
applyBindingsToNodeAndDescendantsInternal(getBindingContext(viewModelOrBindingContext), rootNode, true);
};
这里主要做了几件事很明显加载jq对象 如果没定义dom容器将dom容器设置为Body我就不多解释了由于在1个js文件中F12进applyBindingsToNodeAndDescendantsInternal方法
代码如下:
function applyBindingsToNodeAndDescendantsInternal (bindingContext, nodeVerified, bindingContextMayDifferFromDomParentElement) {
var shouldBindDescendants = true;
// Perf optimisation: Apply bindings only if...
// (1) We need to store the binding context on this node (because it may differ from the DOM parent node's binding context)
// Note that we can't store binding contexts on non-elements (e.g., text nodes), as IE doesn't allow expando properties for those
// (2) It might have bindings (e.g., it has a data-bind attribute, or it's a marker for a containerless template)
var isElement = (nodeVerified.nodeType === 1);
if (isElement) // Workaround IE <= 8 HTML parsing weirdness
ko.virtualElements.normaliseVirtualElementDomStructure(nodeVerified);
var shouldApplyBindings = (isElement && bindingContextMayDifferFromDomParentElement) // Case (1)
|| ko.bindingProvider['instance']['nodeHasBindings'](nodeVerified); // Case (2)
if (shouldApplyBindings)
shouldBindDescendants = applyBindingsToNodeInternal(nodeVerified, null, bindingContext, bindingContextMayDifferFromDomParentElement)['shouldBindDescendants'];
if (shouldBindDescendants && !bindingDoesNotRecurseIntoElementTypes[ko.utils.tagNameLower(nodeVerified)]) {
// We're recursing automatically into (real or virtual) child nodes without changing binding contexts. So,
// * For children of a *real* element, the binding context is certainly the same as on their DOM .parentNode,
// hence bindingContextsMayDifferFromDomParentElement is false
// * For children of a *virtual* element, we can't be sure. Evaluating .parentNode on those children may
// skip over any number of intermediate virtual elements, any of which might define a custom binding context,
// hence bindingContextsMayDifferFromDomParentElement is true
applyBindingsToDescendantsInternal(bindingContext, nodeVerified, /* bindingContextsMayDifferFromDomParentElement: */ !isElement);
}
}
在初次分析源代码的时候有个技巧就是不要追根究底 根据注释和方面名称其实就可以读懂一些东西,过滤到一些不是很重要的东西可以更快的抓住本质。同时我们更关系的是结果,所以这里使用浏览器调试。如图
在控制台里输入bindingContext发现bindContext已经从最初的viewModel变成了
而这里的变化是在最初判定jquery对象里处理的 不知道大家有没有注意 有个getBindingContext方法至于为什么会变成这种数据结构这里我会后面分析,这篇分析的重点是攻略MVVM实现的本质。F12进去之后如下代码
function applyBindingsToDescendantsInternal (bindingContext, elementOrVirtualElement, bindingContextsMayDifferFromDomParentElement) {
var currentChild,
nextInQueue = ko.virtualElements.firstChild(elementOrVirtualElement),
provider = ko.bindingProvider['instance'],
preprocessNode = provider['preprocessNode'];
// Preprocessing allows a binding provider to mutate a node before bindings are applied to it. For example it's
// possible to insert new siblings after it, and/or replace the node with a different one. This can be used to
// implement custom binding syntaxes, such as {{ value }} for string interpolation, or custom element types that
// trigger insertion of <template> contents at that point in the document.
if (preprocessNode) {
while (currentChild = nextInQueue) {
nextInQueue = ko.virtualElements.nextSibling(currentChild);
preprocessNode.call(provider, currentChild);
}
// Reset nextInQueue for the next loop
nextInQueue = ko.virtualElements.firstChild(elementOrVirtualElement);
}
while (currentChild = nextInQueue) {
// Keep a record of the next child *before* applying bindings, in case the binding removes the current child from its position
nextInQueue = ko.virtualElements.nextSibling(currentChild);
applyBindingsToNodeAndDescendantsInternal(bindingContext, currentChild, bindingContextsMayDifferFromDomParentElement);
}
}
这里发现最终调用的是appyBindingToNodeAndDescendantsInternal方法。而这里的方法实际上就是刚才调用这个方法的方法,也就是没错这里是递归。这段的意义是递归遍历整个容器内的dom树,让每个dom元素都走到applyingBindingsToNodeAndDescendantsInternal那么问题来了注册观察者在哪里注册的。实际上在
applyingBindingsToNodeAndDescendantsInternal的这里
F12进去如下
function applyBindingsToNodeInternal(node, sourceBindings, bindingContext, bindingContextMayDifferFromDomParentElement) {
// Prevent multiple applyBindings calls for the same node, except when a binding value is specified
var alreadyBound = ko.utils.domData.get(node, boundElementDomDataKey);
if (!sourceBindings) {
if (alreadyBound) {
throw Error("You cannot apply bindings multiple times to the same element.");
}
ko.utils.domData.set(node, boundElementDomDataKey, true);
}
// Optimization: Don't store the binding context on this node if it's definitely the same as on node.parentNode, because
// we can easily recover it just by scanning up the node's ancestors in the DOM
// (note: here, parent node means "real DOM parent" not "virtual parent", as there's no O(1) way to find the virtual parent)
if (!alreadyBound && bindingContextMayDifferFromDomParentElement)
ko.storedBindingContextForNode(node, bindingContext);
// Use bindings if given, otherwise fall back on asking the bindings provider to give us some bindings
var bindings;
if (sourceBindings && typeof sourceBindings !== 'function') {
bindings = sourceBindings;
} else {
var provider = ko.bindingProvider['instance'],
getBindings = provider['getBindingAccessors'] || getBindingsAndMakeAccessors;
// Get the binding from the provider within a computed observable so that we can update the bindings whenever
// the binding context is updated or if the binding provider accesses observables.
var bindingsUpdater = ko.dependentObservable(
function() {
bindings = sourceBindings ? sourceBindings(bindingContext, node) : getBindings.call(provider, node, bindingContext);
// Register a dependency on the binding context to support obsevable view models.
if (bindings && bindingContext._subscribable)
bindingContext._subscribable();
return bindings;
},
null, { disposeWhenNodeIsRemoved: node }
);
if (!bindings || !bindingsUpdater.isActive())
bindingsUpdater = null;
}
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() {
return evaluateValueAccessor(bindingsUpdater()[bindingKey]);
};
} : function(bindingKey) {
return bindings[bindingKey];
};
// Use of allBindings as a function is maintained for backwards compatibility, but its use is deprecated
function allBindings() {
return ko.utils.objectMap(bindingsUpdater ? bindingsUpdater() : bindings, evaluateValueAccessor);
}
// The following is the 3.x allBindings API
allBindings['get'] = function(key) {
return bindings[key] && evaluateValueAccessor(getValueAccessor(key));
};
allBindings['has'] = function(key) {
return key in bindings;
};
// First put the bindings into the right order
var orderedBindings = topologicalSortBindings(bindings);
// Go through the sorted bindings, calling init and update for each
ko.utils.arrayForEach(orderedBindings, function(bindingKeyAndHandler) {
// Note that topologicalSortBindings has already filtered out any nonexistent binding handlers,
// so bindingKeyAndHandler.handler will always be nonnull.
var handlerInitFn = bindingKeyAndHandler.handler["init"],
handlerUpdateFn = bindingKeyAndHandler.handler["update"],
bindingKey = bindingKeyAndHandler.key;
if (node.nodeType === 8) {
validateThatBindingIsAllowedForVirtualElements(bindingKey);
}
try {
// Run init, ignoring any dependencies
if (typeof handlerInitFn == "function") {
ko.dependencyDetection.ignore(function() {
var initResult = handlerInitFn(node, getValueAccessor(bindingKey), allBindings, bindingContext['$data'], bindingContext);
// If this binding handler claims to control descendant bindings, make a note of this
if (initResult && initResult['controlsDescendantBindings']) {
if (bindingHandlerThatControlsDescendantBindings !== undefined)
throw new Error("Multiple bindings (" + bindingHandlerThatControlsDescendantBindings + " and " + bindingKey + ") are trying to control descendant bindings of the same element. You cannot use these bindings together on the same element.");
bindingHandlerThatControlsDescendantBindings = bindingKey;
}
});
}
// Run update in its own computed wrapper
if (typeof handlerUpdateFn == "function") {
ko.dependentObservable(
function() {
handlerUpdateFn(node, getValueAccessor(bindingKey), allBindings, bindingContext['$data'], bindingContext);
},
null,
{ disposeWhenNodeIsRemoved: node }
);
}
} catch (ex) {
ex.message = "Unable to process binding \"" + bindingKey + ": " + bindings[bindingKey] + "\"\nMessage: " + ex.message;
throw ex;
}
});
}
return {
'shouldBindDescendants': bindingHandlerThatControlsDescendantBindings === undefined
};
};
这个方法我认为可以说是整个ko的核心了注册dom观察者解析dom元素节点 解析data-bind都在这个方法里面。
下篇我会用一篇单独对这个方法进行解析,说明MvvM的核心。