在ng-repeat指令内使用指令,并利用作用域'@'的神秘力量

62
如果您想在工作代码中查看问题,请从这里开始:http://jsbin.com/ayigub/2/edit 考虑以下几种编写简单指令的几乎等效方式:
app.directive("drinkShortcut", function() {
  return {
    scope: { flavor: '@'},
    template: '<div>{{flavor}}</div>'
  };
});

app.directive("drinkLonghand", function() {
  return {
    scope: {},
    template: '<div>{{flavor}}</div>',
    link: function(scope, element, attrs) {
      scope.flavor = attrs.flavor;
    }
  };
});

当它们单独使用时,这两个指令的作用和行为是相同的:

  <!-- This works -->
  <div drink-shortcut flavor="blueberry"></div>
  <hr/>

  <!-- This works -->
  <div drink-longhand flavor="strawberry"></div>
  <hr/>

然而,当在ng-repeat中使用时,只有简写版本有效:

  <!-- Using the shortcut inside a repeat also works -->
  <div ng-repeat="flav in ['cherry', 'grape']">
    <div drink-shortcut flavor="{{flav}}"></div>
  </div>
  <hr/>

  <!-- HOWEVER: using the longhand inside a repeat DOESN'T WORK -->      
  <div ng-repeat="flav in ['cherry', 'grape']">
    <div drink-longhand flavor="{{flav}}"></div>
  </div>

我的问题是:

  1. 为什么长手写法在ng-repeat内部无法工作?
  2. 如何使长手写法在ng-repeat内部工作?
1个回答

104

drinkLonghand 中,你使用了以下代码

scope.flavor = attrs.flavor;

在链接阶段,插值属性尚未被评估,因此它们的值为undefined。(它们在ng-repeat之外工作是因为在这些实例中,您不使用字符串插值; 您只是传递一个普通的字符串,例如“strawberry”)。这在指令开发人员指南中提到,以及$observe上的一种方法,在API文档中不存在:

使用$observe观察包含插值(例如src="{{bar}}")的属性的值更改。这不仅非常高效,而且也是唯一轻松获取实际值的方法,因为在链接阶段插值尚未评估,因此该值此时设置为undefined

因此,要解决问题,您的drinkLonghand指令应如下所示:

app.directive("drinkLonghand", function() {
  return {
    template: '<div>{{flavor}}</div>',
    link: function(scope, element, attrs) {
      attrs.$observe('flavor', function(flavor) {
        scope.flavor = flavor;
      });
    }
  };
});

然而,此方法的问题在于它不使用隔离作用域;因此,这行代码

scope.flavor = flavor;

这段代码有可能会覆盖作用域中已存在的名为flavor的变量。添加一个空的隔离作用域也不起作用,因为Angular会尝试基于指令的作用域插值字符串,而其中没有名为flav的属性。(你可以通过在调用attrs.$observe之前添加scope.flav ='test' ; 进行测试。)

当然,您可以通过以下隔离作用域定义来解决这个问题:

scope: { flav: '@flavor' }

或通过创建一个非隔离子作用域

scope: true

或者不依赖于带有{{flavor}}template,而是进行一些直接的DOM操作,比如

attrs.$observe('flavor', function(flavor) {
  element.text(flavor);
});

但这违背了练习的目的(例如,只使用drinkShortcut方法会更容易)。因此,为了使该指令起作用,我们将使用$interpolate服务在指令的$parent作用域上自行进行插值:

app.directive("drinkLonghand", function($interpolate) {
  return {
    scope: {},
    template: '<div>{{flavor}}</div>',
    link: function(scope, element, attrs) {
      // element.attr('flavor') == '{{flav}}'
      // `flav` is defined on `scope.$parent` from the ng-repeat
      var fn = $interpolate(element.attr('flavor'));
      scope.flavor = fn(scope.$parent);
    }
  };
});
当然,这仅适用于 scope.$parent.flav 的初始值;如果该值能够更改,则需要使用 使用 $watch 并重新评估插值函数 fn 的结果(我不确定如何在脑海中准确知道要 $watch 什么;您可能只需传递一个函数)。scope: { flavor: '@' } 是一个很好的快捷方式,可以避免处理所有这些复杂性。 [更新] 回答评论中的问题:

这个快捷方式是如何在后台解决这个问题的?它是否像您所做的那样使用 $interpolate 服务,还是做了其他事情?

我不确定,所以我查看了源代码。我在 compile.js 中找到了以下内容:
forEach(newIsolateScopeDirective.scope, function(definiton, scopeName) {
   var match = definiton.match(LOCAL_REGEXP) || [],
       attrName = match[2]|| scopeName,
       mode = match[1], // @, =, or &
       lastValue,
       parentGet, parentSet;

   switch (mode) {

     case '@': {
       attrs.$observe(attrName, function(value) {
         scope[scopeName] = value;
       });
       attrs.$$observers[attrName].$$scope = parentScope;
       break;
     }

看起来attrs.$observe可以在内部告诉它使用不同的作用域来基于属性观察(上面倒数第二行,在break之前)。虽然尝试自己使用这个功能可能很诱人,但请记住,任何带有双美元符号$$前缀的内容都应该被视为Angular私有API,并且可能会在没有警告的情况下发生更改(当使用@模式时,您已经默认获得了此功能)。


Brandon,这是一个非常棒的答案,谢谢。只有一个后续问题(请随意添加编辑):快捷方式方法是如何在幕后解决此问题的?它是否像您所做的那样使用$interpolate服务,还是在执行其他操作? - Jonah
谢谢!很高兴你这么认为。我之前不太确定,所以找到了解决方案并更新了我的答案。 - Michelle Tilley
1
再次感谢。那是我在SO上收到的对一个有点困难的问题最详细的回答之一,而且正是我想要的。希望我能给它更多的赞。顺便说一句,您可能会对我今天早上提出的这个问题感兴趣,这是关于angular文档中潜在错误的问题,可能比这个问题容易回答:http://stackoverflow.com/questions/16495684/angular-controllers-how-are-these-magic-methods-working-in-this-unit-test - Jonah
很棒的答案!非常感谢。总之,如果您要在ng-repeat中使用指令,应该使用简写方式。 - Greg Grater
作为这个框架中的所有内容,它的工作原理真是一个谜,幸运的是有 Stack Overflow 存在,你可以在那里找到这些答案,但我不停地想,这个框架怎么可能如此不直观呢?人们会花费一半的时间寻找答案/技巧/解决方法/或者你称之为什么,我只希望我今后不必再使用它。 - Charlie Pops

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接