AngularJS $watch在ng-repeat内无法工作。

20 浏览
0 Comments

AngularJS $watch在ng-repeat内无法工作。

API参考作用域页面 中写道:

一个作用域可以继承自父作用域。

开发者指南作用域页面 中写道:

一个作用域(原型式)从它的父作用域中继承属性。

  • 那么,子作用域是否总是通过原型继承从其父作用域继承?
  • 是否有例外情况?
  • 当它继承时,是否总是普通的JavaScript原型继承?
admin 更改状态以发布 2023年5月21日
0
0 Comments

我并不想与马克的答案竞争,只是想强调一下最终让我作为新手理解Javascript继承和原型链的重要部分。

只有属性读取搜索原型链,而不包括属性写入。因此,当您设置

myObject.prop = '123';

它不会查找原型链,但当您设置

myObject.myThing.prop = '123';

写入操作中会进行微妙的读取操作,试图在写入其属性之前查找“myThing”。因此,从子代对对象属性进行写入时还可以访问父对象的属性。

0
0 Comments

快速回答
通常子作用域是原型继承于父作用域的,但并非总是这样。这个规则的一个例外是使用 scope: { ... } 的指令——这会创建一个“隔离”的作用域,它不会原型继承。这种构造在创建“可重用组件”指令时经常使用。

至于细微差别,作用域继承通常很简单...直到你需要在子作用域中使用双向数据绑定(即表单元素,ng-model)。如果你试图从子作用域中绑定到primitive(例如数值,字符串,布尔型)的父作用域中,Ng-repeat、Ng-switch 和 Ng-include 就可能让你失控。这并不像大多数人期望的那样能够正常工作。子作用域获得了其自己的属性,它遮蔽了同名的父属性。你的解决方法包括:

  1. 在父作用域中定义对象用作你的模型,然后在子作用域中引用该对象的属性:parentObj.someProp
  2. 使用 $parent.parentScopeProperty(不总是可能,但比第一种方法更容易)
  3. 在父作用域中定义一个函数,然后从子作用域中调用它(并不总是可能)

新的 AngularJS 开发者常常没有意识到 ng-repeatng-switchng-viewng-includeng-if 都会创建新的子作用域,所以这个问题经常在这些指令涉及时出现。(参见此示例,以快速展示问题。)

通过遵守“永远在您的ng-model中添加 '.'”的“最佳实践”,即可轻松避免原始类型的问题 - 观看价值为3分钟的影片。 Misko使用ng-switch演示了原始绑定问题。

在您的模型中添加“.”将确保使用了原型继承。所以,使用




长答案

JavaScript原型继承

也放在AngularJS维基上: https://github.com/angular/angular.js/wiki/Understanding-Scopes

首先,如果您来自服务器端背景并且更熟悉经典继承,则首先需要对原型继承有坚实的了解。所以,让我们先回顾一下。

假设parentScope具有属性aString、aNumber、anArray、anObject和aFunction。如果childScope原型继承自parentScope,则我们有:

prototypal inheritance

(请注意,为了节省空间,我将anArray对象显示为一个具有其三个值的单个蓝色对象,而不是一个具有三个单独的灰色字面量的单个蓝色对象。)

如果我们尝试从子作用域访问在parentScope上定义的属性,则JavaScript将首先查找子作用域,找不到该属性,然后查找继承的作用域并找到该属性。(如果在parentScope中未找到属性,则会继续往上查找原型链...一直到根作用域)。因此,以下所有内容都是正确的:

childScope.aString === 'parent string'
childScope.anArray[1] === 20
childScope.anObject.property1 === 'parent prop1'
childScope.aFunction() === 'parent output'

假设我们这样做:

childScope.aString = 'child string'

原型链没有被查询,一个新的 aString 属性被添加到 childScope 中。 这个新属性隐藏了与 parentScope 具有相同名称的属性。 当我们讨论下面的ng-repeat和ng-include时,这将变得非常重要。

property hiding

假设我们这样做:

childScope.anArray[1] = '22'
childScope.anObject.property1 = 'child prop1'

原型链被查询,因为对象(anArray 和 anObject)在 childScope 中没有找到。这些对象在 parentScope 中被找到,并且在原始对象上更新属性值。不会向 childScope 添加新属性; 不会创建新对象。 (请注意,在 JavaScript 中,数组和函数也是对象。)

follow the prototype chain

假设我们这样做:

childScope.anArray = [100, 555]
childScope.anObject = { name: 'Mark', country: 'USA' }

原型链没有被查询,child scope 获得两个新的对象属性,这些属性隐藏了 parentScope 对象属性与相同名称。

more property hiding

要点:

  • 如果我们读取 childScope.propertyX,且 childScope 具有 propertyX,则不会查询原型链。
  • 如果我们设置 childScope.propertyX,则不会查询原型链。

最后一个情景:

delete childScope.anArray
childScope.anArray[1] === 22  // true

我们先删除 childScope 属性,然后尝试再次访问该属性时,原型链被查询。

移除子属性后的效果


Angular 作用域的继承

下面是一些创建新作用域并原型继承的指令: ng-repeat, ng-include, ng-switch, ng- controller, 带有scope: true指令, 带有transclude: true 指令

下面是一些创建不继承原型的新作用域的指令: 带有scope:{...}指令。这种指令创建了一个“隔离”的作用域。

注意,默认情况下,指令不会创建新的作用域 - 即默认值是scope: false

ng-include

让我们考虑以下在控制器中的代码:

$scope.myPrimitive = 50;
$scope.myObject    = {aNumber: 11};

以下是HTML代码:



每个ng-include都会生成一个新的子作用域,该作用域从父作用域继承原型属性。

在第一个文本框中输入(例如“77”)会导致子作用域得到一个名为myPrimitive的新属性,该属性会隐藏/遮蔽父作用域中同名的属性。这可能不是您想要/预期的结果。

使用原始值的ng-include

在第二个文本框中输入(例如“99”)不会导致创建新的子作用域属性。因为tpl2.html将模型绑定到一个对象属性,当ngModel查找对象myObject时,原型继承会启动 - 它在父作用域中找到了该属性。

使用对象的ng-include

如果我们不想将我们的模型从原始类型更改为对象,我们可以重新编写第一个模板来使用$parent:


在此输入文本框中输入(例如“22”)并不会导致新的子属性。模型现在绑定到父作用域的一个属性上(因为$parent是一个引用父作用域的子作用域属性)。

ng-include with $parent

对于所有作用域(无论是原型还是否),Angular始终通过作用域属性$parent、$$childHead和$$childTail跟踪父子关系(即层次结构)。通常我不会在图表中显示这些作用域属性。

对于不涉及表单元素的情况,另一种解决方案是在父作用域上定义一个函数来修改原始类型。然后确保子元素始终调用此函数,由于原型继承,子作用域可以访问此函数。例如,

// in the parent scope
$scope.setMyPrimitive = function(value) {
     $scope.myPrimitive = value;
}

这是一个使用此“父函数”方法的示例fiddle。(这个fiddle是作为这个答案的一部分写的:https://stackoverflow.com/a/14104318/215945)。

另请参见https://stackoverflow.com/a/13782671/215945https://github.com/angular/angular.js/issues/1267

ng-switch

ng-switch作用域继承的工作方式与ng-include相同。因此,如果您需要将原始类型的双向数据绑定到父作用域中的原始类型,请使用$parent,或者将模型更改为对象,然后将其绑定到该对象的一个属性。这样可以避免子作用域隐藏/覆盖父作用域属性。

另见AngularJS,绑定Switch-Case的作用域?

ng-repeat

Ng-repeat的功能有些不同。假设我们在我们的控制器中有:

$scope.myArrayOfPrimitives = [ 11, 22 ];
$scope.myArrayOfObjects    = [{num: 101}, {num: 202}]

在我们的HTML中:

对于每个项目/迭代,ng-repeat都会创建一个新的作用域,该作用域从父作用域继承原型,但它还会将该项的值分配给新的子作用域上的新属性。(新属性的名称是循环变量的名称)。这就是ng-repeat的实际Angular源代码:

childScope = scope.$new();  // child scope prototypically inherits from parent scope
...
childScope[valueIdent] = value;  // creates a new childScope property

如果项是原始值 (如myArrayOfPrimitives),则将值的副本分配给新的子作用域属性。改变子作用域属性的值(即使用ng-model,因此子作用域num)并不会改变父作用域引用的数组.所以在第一个ng-repeat中,每个子作用域都获得了一个与myArrayOfPrimitives数组独立的属性:

ng-repeat with primitives

这个ng-repeat不会像你希望/期望的那样工作。在文本框里输入会更改灰色框中的值,这些值只在子作用域中可见。我们想要的是让输入影响myArrayOfPrimitives数组,而不是子作用域基本属性。为了实现这一点,我们需要将模型更改为对象数组。

因此,如果项是一个对象,则将对原始对象(而不是副本)的引用分配给新的子作用域属性。改变子作用域属性的值(即使用ng-model,因此obj.num)会更改父作用域引用的对象。所以在第二个ng-repeat中,我们有:

ng-repeat with objects

(我把一行字涂成灰色只是为了表明它的作用。)

这个程序按照预期工作。在文本框中键入的内容会更改灰色框中的值,这些值对子范围和父范围都是可见的。

请参阅ng-model、ng-repeat和inputs的困难https://stackoverflow.com/a/13782671/215945

ng-controller

使用ng-controller嵌套控制器会导致普通的原型继承,就像使用ng-include和ng-switch一样,因此相同的技术适用。
但是,“通过 $scope 继承共享信息被认为是不好的做法” -- http://onehungrymind.com/angularjs-sticky-notes-pt-1-architecture/
应该使用服务来在控制器之间分享数据。

(如果你真的想通过控制器作用域继承共享数据,那么你不需要做任何事情。子作用域将可以访问所有父作用域属性。
请参阅控制器的加载顺序在加载或导航时不同)

指令

  1. 默认 (scope:false) - 指令不会创建新的作用域,因此这里没有继承。这很容易,但也很危险,因为例如,指令可能认为它正在创建一个新的作用域属性,而实际上它可能会破坏现有的属性。这不是编写旨在作为可重用组件的指令的好选择。
  2. scope:true - 指令创建一个新的子作用域,该子作用域从父作用域继承属性。如果在同一个DOM元素上有多个指令请求新的作用域,则只创建一个新的子作用域。由于我们拥有“普通”的原型继承,这就像ng-include和ng-switch一样,因此要小心使用双向数据绑定到父作用域原始类型,以及子作用域隐藏/遮盖父作用域属性。
  3. scope: {...} - 指令创建一个新的隔离/孤立作用域。它不会从父作用域继承属性。这通常是创建可重用组件的最佳选择,因为指令无法意外读取或修改父作用域。但是,这种指令通常需要访问一些父作用域属性。对象哈希用于在父作用域和隔离作用域之间设置双向绑定(使用“=”)或单向绑定(使用“@”)。还有“&”绑定到父作用域表达式。因此,所有这些都会创建从父作用域派生的本地作用域属性。
    请注意,属性用于帮助设置绑定--你不能仅仅在对象哈希中引用父作用域属性名称,你必须使用一个属性来指定它。例如,如果你想在隔离作用域中绑定父属性parentProp,这不起作用:scope:{localProp:'@parentProp'}。必须使用属性来指定指令要绑定的每个父属性:scope:{localProp:'@theParentProp'}

    隔离作用域__proto__引用对象。
    隔离作用域的$parent引用父作用域,因此尽管它是隔离的并且没有从父作用域继承属性,但它仍然是一个子作用域。

    对于下面的图片,我们有



    scope: { interpolatedProp: '@interpolated', twowayBindingProp: '=twowayBinding' }

    另外,假设指令在其链接函数中执行以下操作:scope.someIsolateProp = "I'm isolated"

    隔离的作用域

    有关隔离作用域的更多信息,请参阅http://onehungrymind.com/angularjs-sticky-notes-pt-2-isolated-scope/
  4. transclude:true - 指令创建一个新的“转录”子作用域,该子作用域从父作用域继承属性。转录的和孤立的作用域(如果有的话)是兄弟--每个作用域的$parent属性引用相同的父作用域。当有转录和隔离作用域时,隔离作用域属性$$nextSibling将引用转录作用域。我不知道有关转录作用域的任何细节。

    对于下面的图片,假设上面的指令添加了以下信息:transclude: true

    转录的作用域

这个代码片段有一个showScope()函数,可以用来检查隔离和转录作用域。请参阅代码片段中的注释以获取说明。

总结

有四种类型的作用域:

  1. 普通原型作用域继承——ng-include、ng-switch、ng-controller、具有scope: true指令
  2. 带有复制/分配的普通原型作用域继承——ng-repeat。每次ng-repeat的迭代都会创建一个新的子作用域,并且该新的子作用域始终获得一个新的属性。
  3. 隔离作用域——带有scope:{...}的指令。这个不是原型继承的,但'='、'@'和'&'提供了一种通过属性访问父作用域属性的机制。
  4. 传递作用域——带有transclude: true指令。这个也是普通原型作用域继承,但它也是任何隔离作用域的同级。

对于所有作用域(原型或非原型),Angular始终通过属性$parent和$$childHead和$$childTail来跟踪父子关系(即层次结构)。

图表使用“*.dot”文件生成,这些文件可以在github上找到。Tim Caswell的“使用对象图学习JavaScript”是使用GraphViz制作图表的灵感来源。

0