Angular.js源码分析之ngif

在angular中有两个内置的指令ng-ifng-repeat是比较特殊的,特殊之处在于他们创建指令时候多了一个内置参数配置:$$tlbng-if相对ng-repeat逻辑则更加简单,分析起来也就更容易。所以这里就来分析分析ng-if指令,目的是不仅要知道了ng-if指令的逻辑,也要知道这个参数为啥会存在。

先来看下源码:

var ngIfDirective = ['$animate', function($animate) {
  return {
    multiElement: true,
    transclude: 'element',
    priority: 600,
    terminal: true,
    restrict: 'A',
    $$tlb: true, // 这个特殊的参数
    link: function($scope, $element, $attr, ctrl, $transclude) {
        var block, childScope, previousElements;
        // 检测值变化
        $scope.$watch($attr.ngIf, function ngIfWatchAction(value) {

          if (value) {
          	// true就是要显示
            if (!childScope) {
            	// 依旧是调用$transclude
              $transclude(function(clone, newScope) {
                childScope = newScope;
                // 插入注释元素
                clone[clone.length++] = document.createComment(' end ngIf: ' + $attr.ngIf + ' ');
                // 保留引用
                block = {
                  clone: clone
                };
                $animate.enter(clone, $element.parent(), $element);
              });
            }
          } else {
          	// 隐藏
          	// 有的话就销毁 且动画移除
            if (previousElements) {
              previousElements.remove();
              previousElements = null;
            }
            if (childScope) {
              childScope.$destroy();
              childScope = null;
            }
            if (block) {
              previousElements = getBlockNodes(block.clone);
              $animate.leave(previousElements).then(function() {
                previousElements = null;
              });
              block = null;
            }
          }
        });
    }
  };
}];

从上边代码可以看出其基本逻辑还是很简单的,但是不明白的是这个私有的设置参数$$tlb到底是用来干嘛的,有啥用处吗?是为了解决什么样的问题的(当然不建议开发者使用)?

我觉得不如首先来看没有这个配置会出现什么问题呢?做一个实验,把ngIfDirective的源码中的$$tlb去掉,然后来一个简单的demo,一探究竟。

<div ng-app="app">
  <button ng-click="toogleV()">toggle visibility</button>
  <div ng-if="showed" class="ifEle" ng-include="url">
    <p ng-class="{showed:showed}">ifEle content</p>
  </div>
</div>
<script type="text/javascript">
var app = angular.module('app', []);
app.run(['$rootScope', function($rootScope) {
  $rootScope.showed = false

  $rootScope.url = './include.html'

  $rootScope.toogleV = function () {
  	$rootScope.showed = !$rootScope.showed;
  };
}])
</script>

include.html内容:

<div ng-class="{showed:showed}">include content</div>

注意上面是在修改后的情况下,打开后发现报错了,这样的错误:

Error: [$compile:multidir] Multiple directives [ngIf, ngInclude] asking for transclusion on: <div ng-if="showed" class="ifEle" ng-include="url">

这样的判断出自:

if (!directive.$$tlb) {
  assertNoDuplicate('transclusion', nonTlbTranscludeDirective, directive, $compileNode);
  nonTlbTranscludeDirective = directive;
}
// 省略
function assertNoDuplicate(what, previousDirective, directive, element) {

  function wrapModuleNameIfDefined(moduleName) {
    return moduleName ?
      (' (module: ' + moduleName + ')') :
      '';
  }

  if (previousDirective) {
    throw $compileMinErr('multidir', 'Multiple directives [{0}{1}, {2}{3}] asking for {4} on: {5}',
        previousDirective.name, wrapModuleNameIfDefined(previousDirective.$$moduleName),
        directive.name, wrapModuleNameIfDefined(directive.$$moduleName), what, startingTag(element));
  }
}

而且transclude节点的时机还是在link之后,去增加或者替换他们。但是实际的情况就是上边demo的场景还是有可能出现的,所以说angular为了解决这个问题加入了私有配置$$tlb,而且仅限于两个指令ng-ifng-repeat

话说回来,有了这个属性,angular就不去检查多个transclude的情况了,就上边的例子来说,究竟是怎样的执行过程呢?从页面结果来看呢,最终执行的结果是显示的同级的include.html的内容。

其实应该拆分来理解,先理解ng-if的指令,他的优先级比较高,所以说是先执行,通过变换并且创建了注释节点,transclude完成之后此时在页面上会展示include处理得到的注释节点(tranclude处理逻辑);然后第二步拿到了in-include的内容后依旧是进行transclude,完成后显示的是原始内容:<p ng-class="{showed:showed}">ifEle content</p>,从之前在ngInclude分析中我们已经知道其实还有一个步骤那就是ngIncludeFillContentDirective的处理,最终的结果就是会给当前的这个ifEle设置innerHTML为得到的模板内容(也就是<div ng-class="{showed:showed}">include content</div>),然后compile和link。

结语

从上边的分析可以知道了$$tlb就是用来指定在compile的时候不要check同一个元素被多次transclude。而ng-if指令本身的逻辑还是比较简单的:监测ng-if属性的值,如果为true了那么就$transclude得到内容,否则的话就移除掉。

发布于: 2015年 10月 27日