AngularJS 源码分析4

来源:互联网 发布:java模板引擎排行 编辑:程序博客网 时间:2024/04/28 03:44

angularjs之$compile

今天主要说说ng里的$compile,这是一个非常关键的服务,页面上的双向绑定,各个监听基本上都是在这里执行的.
源码部分还是引用angular1.2.4,链接在这里下载


compile的源头

ng里最开始引用$compile的地方就是把所有系统内建的指令添加到$CompileProvider里,由于代码太长,只写些关键部分的

$provide.provider('$compile', $CompileProvider).        directive({            a: htmlAnchorDirective,            input: inputDirective,            textarea: inputDirective,            form: formDirective,            script: scriptDirective,            select: selectDirective,            style: styleDirective,            option: optionDirective,            ngBind: ngBindDirective,            ngBindHtml: ngBindHtmlDirective,            ngBindTemplate: ngBindTemplateDirective,            ngClass: ngClassDirective,            ngClassEven: ngClassEvenDirective,            ngClassOdd: ngClassOddDirective,            ngCloak: ngCloakDirective,            ngController: ngControllerDirective,            ngForm: ngFormDirective,            ngHide: ngHideDirective,            ngIf: ngIfDirective,            ngInclude: ngIncludeDirective,            ngInit: ngInitDirective,            ngNonBindable: ngNonBindableDirective,            ngPluralize: ngPluralizeDirective,            ngRepeat: ngRepeatDirective,            ngShow: ngShowDirective,            ngStyle: ngStyleDirective,            ngSwitch: ngSwitchDirective,            ngSwitchWhen: ngSwitchWhenDirective,            ngSwitchDefault: ngSwitchDefaultDirective,            ngOptions: ngOptionsDirective,            ngTransclude: ngTranscludeDirective,            ngModel: ngModelDirective,            ngList: ngListDirective,            ngChange: ngChangeDirective,            required: requiredDirective,            ngRequired: requiredDirective,            ngValue: ngValueDirective        }).        directive({          ngInclude: ngIncludeFillContentDirective        }).        directive(ngAttributeAliasDirectives).        directive(ngEventDirectives);

此处的directive方法就是$CompileProvider里的registerDirective方法,主要就是把内建指令添加到内部的hasDirectives对象内,以方便后面在全局查找指令的时候进行匹配.

compile的启动

启动的方法在这里,只摘取关键代码.

injector.invoke(['$rootScope', '$rootElement', '$compile', '$injector', '$animate',       function(scope, element, compile, injector, animate) {        scope.$apply(function() {          element.data('$injector', injector);          compile(element)(scope);        });      }]    );

上面的代码主要作用就是,初始化相关的依赖,然后执行全局编译,最后更新所有的$watch.

核心的代码就这一句

compile(element)(scope);

其实这里有两步

  • compile(element) 收集完整个页面内的指令,然后返回publicLinkFn函数

  • 执行publicLinkFn(scope) 此处的scope即为$rootScope

先来说说第一步

compile(element)

compile服务返回的是一个构造函数,名为compile,代码在这里

function compile($compileNodes, transcludeFn, maxPriority, ignoreDirective,previousCompileContext) {        if (!($compileNodes instanceof jqLite)) {          $compileNodes = jqLite($compileNodes);        }        forEach($compileNodes, function(node, index){          if (node.nodeType == 3 && node.nodeValue.match(/\S+/)  ) {            $compileNodes[index] = node = jqLite(node).wrap('<span></span>').parent()[0];          }        });        var compositeLinkFn =                compileNodes($compileNodes, transcludeFn, $compileNodes,                             maxPriority, ignoreDirective, previousCompileContext);        safeAddClass($compileNodes, 'ng-scope');        return function publicLinkFn(scope, cloneConnectFn, transcludeControllers){            // 代码太长,省略        };    }

从上面的代码可以看出,如果要查找的节点是文本元素,则包装一个span标签,然后执行compileNodes,这个方法主要是收集指令.

function compileNodes(nodeList, transcludeFn, $rootElement, maxPriority, ignoreDirective,previousCompileContext) {          var linkFns = [],              attrs, directives, nodeLinkFn, childNodes, childLinkFn, linkFnFound;          for (var i = 0; i < nodeList.length; i++) {                attrs = new Attributes();                directives = collectDirectives(nodeList[i], [], attrs, i === 0 ? maxPriority : undefined,ignoreDirective);                nodeLinkFn = (directives.length)                    ? applyDirectivesToNode(directives, nodeList[i], attrs, transcludeFn, $rootElement,null, [], [], previousCompileContext)                    : null;                if (nodeLinkFn && nodeLinkFn.scope) {                  safeAddClass(jqLite(nodeList[i]), 'ng-scope');                }                childLinkFn = (nodeLinkFn && nodeLinkFn.terminal ||                              !(childNodes = nodeList[i].childNodes) ||                              !childNodes.length)                    ? null                    : compileNodes(childNodes,                         nodeLinkFn ? nodeLinkFn.transclude : transcludeFn);                linkFns.push(nodeLinkFn, childLinkFn);                linkFnFound = linkFnFound || nodeLinkFn || childLinkFn;                //use the previous context only for the first element in the virtual group                previousCompileContext = null;          }          return linkFnFound ? compositeLinkFn : null;          function compositeLinkFn(scope, nodeList, $rootElement, boundTranscludeFn) {                // 代码省略           }    }

上面的编译节点的主要流程就是,先通过collectDirectives搜集当前节点的指令,然后找到了,则调用applyDirectivesToNode来应用指令,然后查找当前节点的子节点是否有指令,这是一个递归,最后把所有的函数添加到一个内部的linkFns数组中,这个将在最后链接的时候会用到.

先来看看collectDirectives方法,这个方法代码比较长,就不贴了,直接说代码逻辑

ng收集指令的时候,首先根据节点类型

  • element_node 1 先根据tagName来添加指令,然后loop节点的attrs来添加指令,最后通过className来添加指令

  • text_node 3 假如是文本节点的话,则调用addTextInterpolateDirective方法来构建指令

function addTextInterpolateDirective(directives, text) {      var interpolateFn = $interpolate(text, true);      if (interpolateFn) {        directives.push({          priority: 0,          compile: valueFn(function textInterpolateLinkFn(scope, node) {            var parent = node.parent(),                bindings = parent.data('$binding') || [];            bindings.push(interpolateFn);            safeAddClass(parent.data('$binding', bindings), 'ng-binding');            scope.$watch(interpolateFn, function interpolateFnWatchAction(value) {              node[0].nodeValue = value;            });          })        });      }    }

像这样的文本节点就会自动构建上面的指令,自动添加一个监听,通过修改原生方法来修改节点的值

<body>    {{ feenan }}</body>
  • comment 8 注释的节点也能自动的添加指令

上面的三种情况的添加指令方法是addDirective

function addDirective(tDirectives, name, location, maxPriority, ignoreDirective, startAttrName,endAttrName) {      if (name === ignoreDirective) return null;      var match = null;      if (hasDirectives.hasOwnProperty(name)) {        for(var directive, directives = $injector.get(name + Suffix),            i = 0, ii = directives.length; i<ii; i++) {          try {            directive = directives[i];            if ( (maxPriority === undefined || maxPriority > directive.priority) &&                 directive.restrict.indexOf(location) != -1) {              if (startAttrName) {                directive = inherit(directive, {$$start: startAttrName, $$end: endAttrName});              }              tDirectives.push(directive);              match = directive;            }          } catch(e) { $exceptionHandler(e); }        }      }      return match;    }

这里就会用到这个对象hasDirectives,这就是系统在初始化的时候添加的一个内健指令对象集合.
假如节点的名称在这个对象,则把指令添加到传递进来的tDirectives数组内.返回当前指令.

搜集完指令之后,就要开始使用了,接下来调用applyDirectivesToNode方法,这个方法将会生成最终链接时候调用的link函数

applyDirectivesToNode会对directives进行loop,依次检查指令的属性,这里以compile属性来说,当检测到指令有compile属性,则

if (directive.compile) {              try {                linkFn = directive.compile($compileNode, templateAttrs, childTranscludeFn);                if (isFunction(linkFn)) {                  addLinkFns(null, linkFn, attrStart, attrEnd);                } else if (linkFn) {                  addLinkFns(linkFn.pre, linkFn.post, attrStart, attrEnd);                }              } catch (e) {                $exceptionHandler(e, startingTag($compileNode));              }            }

执行directive.compile方法,返回一个linkFn,然后调用addLinkFns添加到内部数组中,这里是postLinkFns数组,最终执行用户定义的linkFn或者系统自带的,都会访问这个数组的内容

最后applyDirectivesToNode返回的是一个内部函数nodeLinkFn,这个就是调用用户定义指令函数的发起者.

当前节点指令处理完之后,然后开始查找子节点的指令,基本上跟父节点规则一样,最后返回compositeLinkFn函数给compositeLinkFn内部变量,这个下面会用到,最后整个compile函数返回publicLinkFn函数

到这里compile(element)就执行完了,再来说说第二步,最终进行指令链接

publicLinkFn(scope)

首先scope是根作用域,这个方法主要是执行所有的链接函数,添加监听函数.

function publicLinkFn(scope, cloneConnectFn, transcludeControllers){    assertArg(scope, 'scope');    var $linkNode = cloneConnectFn      ? JQLitePrototype.clone.call($compileNodes) // IMPORTANT!!!      : $compileNodes;    forEach(transcludeControllers, function(instance, name) {      $linkNode.data('$' + name + 'Controller', instance);    });    // Attach scope only to non-text nodes.    for(var i = 0, ii = $linkNode.length; i<ii; i++) {      var node = $linkNode[i],          nodeType = node.nodeType;      if (nodeType === 1  || nodeType === 9 ) {        $linkNode.eq(i).data('$scope', scope);      }    }    if (cloneConnectFn) cloneConnectFn($linkNode, scope);    if (compositeLinkFn) compositeLinkFn(scope, $linkNode, $linkNode);    return $linkNode;};

把当前作用域保存到元素的data里,然后调用第一步里的compositeLinkFn函数,传递根作用域和根节点

这个会调用compileNodes里的compositeLinkFn方法,此时闭包属性linkFns属性里保存了两个项nodeLinkFn, childLinkFn,根节点的nodelinkFn为空,childlinkFn有值,它的值本身也是一个compositeLinkFn函数,然后传递根节点的子节点进去,最终当nodelinkFn有值的时候,会调用applyDirectivesToNode内部的nodeLinkFn方法,上面说了,这个调用所有链接函数的发起者.

nodeLinkFn代码比较长,就不贴了,这里主要做了以下几件事

  • 根据指令的scope属性来构建作用域信息

  • 是否需要构建控制器,此时会调用控制器的初始化信息

  • 执行prelinkfns,postlinkfns数组内的链接函数,这些都是在第一步收集好的

核心代码如下

// PRELINKINGfor(i = 0, ii = preLinkFns.length; i < ii; i++) {  try {    linkFn = preLinkFns[i];    linkFn(linkFn.isolateScope ? isolateScope : scope, $element, attrs,        linkFn.require && getControllers(linkFn.require, $element, elementControllers), transcludeFn);  } catch (e) {    $exceptionHandler(e, startingTag($element));  }}// RECURSION// We only pass the isolate scope, if the isolate directive has a template,// otherwise the child elements do not belong to the isolate directive.var scopeToChild = scope;if (newIsolateScopeDirective && (newIsolateScopeDirective.template || newIsolateScopeDirective.templateUrl === null)) {  scopeToChild = isolateScope;}childLinkFn && childLinkFn(scopeToChild, linkNode.childNodes, undefined, boundTranscludeFn);// POSTLINKINGfor(i = postLinkFns.length - 1; i >= 0; i--) {  try {    linkFn = postLinkFns[i];    linkFn(linkFn.isolateScope ? isolateScope : scope, $element, attrs,        linkFn.require && getControllers(linkFn.require, $element, elementControllers), transcludeFn);  } catch (e) {    $exceptionHandler(e, startingTag($element));  }}

执行流程为preLinkFns -> childLinkFn -> postLinkFns

最终执行链接的函数在这里

linkFn(linkFn.isolateScope ? isolateScope : scope, $element, attrs,        linkFn.require && getControllers(linkFn.require, $element, elementControllers), transcludeFn);

这里就会执行用户自定义的指令内容,以及系统自带的指令内容,像上面文本节点对应的指令内容,像下面的这个

function textInterpolateLinkFn(scope, node) {    var parent = node.parent(),        bindings = parent.data('$binding') || [];    bindings.push(interpolateFn);    safeAddClass(parent.data('$binding', bindings), 'ng-binding');    scope.$watch(interpolateFn, function interpolateFnWatchAction(value) {      node[0].nodeValue = value;    });}

指令内容里可以添加监听,写一些DOM操作的代码,都是可以的

总结

以上只是对编译服务的一些简单理解,有啥错误的希望大家指出来,一起进步,以后有空再分析下业务相关的Provider.

欢迎转载,转载请注明作者和出处:feenan

1 0
原创粉丝点击