在ng-repeat中使用指令,以及作用域“@”的神秘力量

如果你喜欢在工作代码中看到问题,请从这里开始: http : //jsbin.com/ayigub/2/edit

考虑这个几乎相当的方法来写一个简单的direcive:

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. 为什么long-version不能在ng-repeat中工作?
  2. 你怎么能在ng-repeat里面做这个longhand版本?

drinkLonghand ,你使用的代码

 scope.flavor = attrs.flavor; 

在链接阶段,内插属性还没有被评估,所以它们的值是undefined 。 (他们在ng-repeat之外工作,因为在这些情况下,你没有使用string插值;你只是传递一个普通的普通string,例如“草莓”。)这是指令开发人员指南中提到的,方法在API文档中不存在的名为$observe Attributes

使用$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的范围上的预先存在的variables。 添加空白隔离范围也不起作用; 这是因为Angular试图根据指令的作用域插入string,在这个作用域上没有叫做flav属性。 (您可以通过添加scope.flav = 'test';在对attrs.$observe的调用之scope.flav = 'test';

当然,你可以用一个隔离作用域定义来解决这个问题

 scope: { flav: '@flavor' } 

或者通过创build一个非孤立的子范围

 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的初始值scope.$parent.flav ; 如果这个值能够改变的话,你必须使用$watch并重新评估插值函数fn的结果(我没有积极的离开我的头顶,你怎么知道要看什么;你可能只是必须通过一个函数)。 scope: { flavor: '@' }是避免pipe理所有这些复杂性的一个很好的捷径。

[更新]

从评论中回答这个问题:

快捷方式如何在幕后解决这个问题? 它是像你一样使用$ 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可以在内部被告知使用与当前不同的范围来将属性观察build立在(下一个最后一行,在break之上)的基础上。 虽然自己可能很想使用它,但请记住,任何带有双美元的$$前缀的东西都应该被认为是Angular的私有API的私有API,并且可以随时更改而不会发出警告(更不用说,使用@模式)。