Angular.js源码分析之nginclude

还记得在源码分析之compile中在分析$CompileProvider的实例directive(也就是module实例可以用来自定义指令的directive)方法的时候,里边说到对于每自定义一个指令其实都会有对应的Provider存在,这里再次看下那部分代码:

/**
 * 注册新的指令
 */
 this.directive = function registerDirective(name, directiveFactory) {
  assertNotHasOwnProperty(name, 'directive');
  if (isString(name)) {
    // key value 形式
    assertValidDirectiveName(name);
    assertArg(directiveFactory, 'directiveFactory');
    if (!hasDirectives.hasOwnProperty(name)) {
      // 还没有name的Directive工厂
      hasDirectives[name] = [];
      // 加后缀Directive
      $provide.factory(name + Suffix, ['$injector', '$exceptionHandler',
        function($injector, $exceptionHandler) {
          // 此时使用的时候
          // 取得所有的directiveFactory然后执行
          // 得到的就是要如何构建指令的对象(指令对象)
          var directives = [];
          forEach(hasDirectives[name], function(directiveFactory, index) {
            try {
              var directive = $injector.invoke(directiveFactory);
              if (isFunction(directive)) {
                directive = { compile: valueFn(directive) };
              } else if (!directive.compile && directive.link) {
                directive.compile = valueFn(directive.link);
              }
              // 指令对象的配置属性们
              directive.priority = directive.priority || 0;
              directive.index = index;
              directive.name = directive.name || name;
              directive.require = directive.require || (directive.controller && directive.name);
              directive.restrict = directive.restrict || 'EA';
              // 解析scope(bindToController)绑定
              var bindings = directive.$$bindings =
                  parseDirectiveBindings(directive, directive.name);
              if (isObject(bindings.isolateScope)) {
                // 独立scope对象
                directive.$$isolateBindings = bindings.isolateScope;
              }
              // 指定directive的$$moduleName
              // 也就是在moduleInstance对象上暴露directive的时候使用的是
              // invokeLaterAndSetModuleName 给directiveFactory赋值了$$moduleName
              directive.$$moduleName = directiveFactory.$$moduleName;
              directives.push(directive);
            } catch (e) {
              $exceptionHandler(e);
            }
          });
          return directives;
        }]);
    }
    // 添加
    hasDirectives[name].push(directiveFactory);
  } else {
    // 批量注册 指令
    forEach(name, reverseParams(registerDirective));
  }
  return this;
};

在那篇文章中有这样说:

从上边可以看出在调用directive的时候其实是会创建一个以name+Suffix为名的service的(通过$provide.factory的方法),也就意味着同一个名字的指令可以有多个directiveFactory的,也就说我们可以一直增强同一个指令。

而在angular中一个指令被定义多次的就有这样的一个指令ngInclude,也就是本篇要分析的重点。

先看定义部分:

$provide.provider('$compile', $CompileProvider).
  directive({
    // 省略
    ngInclude: ngIncludeDirective,
    // 省略
  }).
  directive({
    ngInclude: ngIncludeFillContentDirective
  })

第一次定义是用的ngIncludeDirective,第二次是ngIncludeFillContentDirective,下边就分别来分析分析他们。

ngIncludeDirective

看代码:

var ngIncludeDirective = ['$templateRequest', '$anchorScroll', '$animate',
                  function($templateRequest,   $anchorScroll,   $animate) {
  return {
    restrict: 'ECA',
    priority: 400,
    terminal: true,// 终止
    transclude: 'element',
    controller: angular.noop,
    compile: function(element, attr) {
      var srcExp = attr.ngInclude || attr.src,
          onloadExp = attr.onload || '',
          autoScrollExp = attr.autoscroll;
      // link函数
      return function(scope, $element, $attr, ctrl, $transclude) {
        var changeCounter = 0,
            currentScope,
            previousElement,
            currentElement;
        
        // 清除上一个
        var cleanupLastIncludeContent = function() {
          if (previousElement) {
            previousElement.remove();
            previousElement = null;
          }
          if (currentScope) {
            currentScope.$destroy();
            currentScope = null;
          }
          if (currentElement) {
            $animate.leave(currentElement).then(function() {
              previousElement = null;
            });
            previousElement = currentElement;
            currentElement = null;
          }
        };
        // 监控其src的值  如果发生变化了需要重新请求得到内容
        // 然后赋值给controller.template
        scope.$watch(srcExp, function ngIncludeWatchAction(src) {
          var afterAnimation = function() {
            if (isDefined(autoScrollExp) && (!autoScrollExp || scope.$eval(autoScrollExp))) {
              $anchorScroll();
            }
          };
          var thisChangeId = ++changeCounter;

          if (src) {
            //set the 2nd param to true to ignore the template request error so that the inner
            //contents and scope can be cleaned up.
            $templateRequest(src, true).then(function(response) {
              if (thisChangeId !== changeCounter) return;
              var newScope = scope.$new();
              ctrl.template = response;
              // 注意没有对response的做其他处理了
              // 真正的处理其实是在ngIncludeFillContentDirective里边

              // 调用$transclude 传入新的scope
              // 关键是这里调用了$transclude
              var clone = $transclude(newScope, function(clone) {
                cleanupLastIncludeContent();
                $animate.enter(clone, null, $element).then(afterAnimation);
              });

              currentScope = newScope;
              currentElement = clone;

              currentScope.$emit('$includeContentLoaded', src);
              scope.$eval(onloadExp);
            }, function() {
              if (thisChangeId === changeCounter) {
                cleanupLastIncludeContent();
                scope.$emit('$includeContentError', src);
              }
            });
            scope.$emit('$includeContentRequested', src);
          } else {
            cleanupLastIncludeContent();
            ctrl.template = null;
          }
        });
      };
    }
  };
}];

这里先不说关键点,以及整个过程,先来看看ngIncludeFillContentDirective大概定义。

ngIncludeFillContentDirective

看代码:

var ngIncludeFillContentDirective = ['$compile',
  function($compile) {
    return {
      restrict: 'ECA',
      priority: -400,
      require: 'ngInclude',
      link: function(scope, $element, $attr, ctrl) {
        // 很简单 编译得到的内容 然后link到当然scope
        if (/SVG/.test($element[0].toString())) {
          // WebKit: https://bugs.webkit.org/show_bug.cgi?id=135698 --- SVG elements do not
          // support innerHTML, so detect this here and try to generate the contents
          // specially.
          $element.empty();

          $compile(jqLiteBuildFragment(ctrl.template, document).childNodes)(scope,
              function namespaceAdaptedClone(clone) {
            $element.append(clone);
          }, {futureParentElement: $element});
          return;
        }
        $element.html(ctrl.template);
        $compile($element.contents())(scope);
      }
    };
  }];

过程分析

OK,以及看过了两次定义,记个大概就好,下边来一段示例,具体分析下整个过程。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>angular ng-include demo</title>
  <script type="text/javascript" src="angular.js"></script>
</head>
<body>
  <div ng-app="app">
    <div ng-include="url" class="includeEle"></div>
  </div>
  <script type="text/javascript">
  var app = angular.module('app', []);
  app.run(['$rootScope', function($rootScope) {
    $rootScope.url = './include.html'
  }])
  </script>
</body>
</html>

假设include.html:

<p>include content</p>

其实很简单的demo,没有其他干扰。

  1. 页面load之后,初始化整个应用,而初始化的最后一步就是compile & link,先执行compile,返回的publicLinkFn1

  2. compile从ng-app=”app”的元素(rootElement)开始,查看该div本身是没有指令的,但是他有childNodes,所以递归调用compileNodes(返回childLinkFn1,也就是指向的递归调用compileNodes的返回值compositeLinkFn2),返回compositeLinkFn1

1. 有效的childNodes(因为也有换行的文本节点也会被解析parse)就是includeEle,然后收集指令发现有ng-include,其实得到的结果是有两个指令directives的,名字都为ng-include,然后调用applyDirectivesToNode得到nodeLinkFn1

  1. 遍历directives,发现第一个指令(也就是上边的`ngIncludeDirective`)是有transclude的,而且是'element',所以说为了得到childTranscludeFn1,需要递归调用compile(传入当前节点元素,同时也传入了当前指令的优先级priority,这样避免当前当前节点被重复应用当前的这个指令),返回的函数publicLinkFn2就是childTranscludeFn1

    1. 依旧是类似的步骤(本质递归的过程):compileNodes,然后收集指令,由于传入了优先级priority,所以说这次收集得到的指令directives只有一个,也就是上边的`ngIncludeFillContentDirective`,应用指令(返回的是nodeLinkFn2),返回compositeLinkFn3

  1. `ngIncludeDirective`带有terminal,所以就直接终止了
  1. 注意此时会给nodeLinkFn1设置属性transclude的值为childTranscludeFn1,也就是递归调用compile得到的publicLinkFn2

  2. compile阶段依旧完成了,紧接着就是link了,然后会调用到rootElement返回的publicLinkFn1

  3. publicLinkFn1会去调用compositeLinkFn1

1. 在compositeLinkFn1中会处理childLinkFn1,而childLinkFn1其实就是compositeLinkFn2,他会调用应用节点到指令`ngIncludeDirective`的返回的nodeLinkFn1

  1. 在nodeLinkFn1中会执行指令中的preLink,childLinkFn(此时没有),然后postLinks(逻辑就在`ngIncludeDirective`定义中的,他通过$templateRequest,得到模板内容,然后调用$transclude)

  1. nodeLinkFn1被调用的之前会得到一个boundTranscludeFn,__可以理解为__之前compile的时候的childTranscludeFn1,也就是publicLinkFn2,所以说在上一步$templateRequest的逻辑中调用的$transclude就会调用publicLinkFn2

  1. publicLinkFn2的处理逻辑依旧类似:compositeLinkFn3 -> nodeLinkFn2 -> ngIncludeFillContentDirective的postLink函数:编译内容(`<p>include content</p>`),然后link

虽然说设置的场景已经摒弃了其他的感染,但是整个过程还是很复杂的,基本上都是一个递归处理的过程。

结语

虽然本篇要介绍的内容是关于ngInclude这个指令的源码分析,但是其实这不是主要目的,主要目的还是利用这个特殊的指令来进一步帮助我们理解angular的compile和link。

发布于: 2015年 10月 25日