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 中工作?
4

1 回答 1

104

drinkLonghand中,您使用代码

scope.flavor = attrs.flavor;

在链接阶段,插值属性尚未评估,因此它们的值为undefined. (它们在 之外工作,ng-repeat因为在这些情况下,您没有使用字符串插值;您只是传递了一个普通的普通字符串,例如“草莓”。)指令开发人员指南中提到了这一点,以及关于该方法的Attributes方法在 API 文档中不存在,称为$observe

用于$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。(您可以通过scope.flav = 'test';在调用上方添加对. 进行测试attrs.$observe。)

当然,您可以使用隔离范围定义来解决此问题,例如

scope: { flav: '@flavor' }

或通过创建非隔离子范围

scope: true

或者不依赖templatewith{{flavor}}而是做一些直接的 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并重新评估 interpolate 函数的结果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 的私有内容,并且可能会在没有警告的情况下进行更改(更不用说在使用@模式)。

于 2013-05-11T23:59:54.790 回答