KnockOut源码精析 一个简单的例子(一)

好久没更一些有质量的博文,而这段时间我想写一些有意义的东西。希望能对刚接触前段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的核心。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值