深入理解 AngularJS 的 Scope

前端之家收集整理的这篇文章主要介绍了深入理解 AngularJS 的 Scope前端之家小编觉得挺不错的,现在分享给大家,也给大家做个参考。

本文源地址链接http://www.lovelucy.info/understanding-scopes-in-angularjs.html

本文作者:lovelucy

声明:本文为转载,请尊重原创,如您也需要转载,请注明源地址,而不要注明我的博客地址,谢谢。

一、遇到的问题

问题发生在使用 AngularJS 嵌套 Controller 的时候。因为每个 Controller 都有它对应的 Scope(相当于作用域、控制范围),所以 Controller 的嵌套,也就意味着 Scope 的嵌套。这个时候如果两个 Scope 内都有同名的 Model 会发生什么呢?从子 Scope 怎样更新父 Scope 里的 Model 呢?

这个问题很典型,比方说当前页面是一个产品列表,那么就需要定义一个 ProductListController

function ProductListController($scope, $http) {
    $http.get('/api/products.json')
        .success(function(data){
            $scope.productList = data;
        });
    $scope.selectedProduct = {};
}

你大概看到了在 Scope 里还定义了一个 selectedProduct 的 Model,表示选中了某一个产品。这时会获取该产品详情,而页面通过 AngularJS 中的$routeProvider自动更新,拉取新的详情页模板,模板中有一个 ProductDetailController

function ProductDetailController $routeParams'/api/products/'+$routeParams.productId+'.json' 有趣的事情发生了,在这里也有一个 selectedProduct ,它会怎样影响 ProductListController 中的 selectedProduct 呢?

答案是没有影响。在 AnuglarJS 里子 Scope 确实会继承父 Scope 中的对象,但当你试下对基本数据类型(string,number,boolean)的双向数据绑定时,就会发现一些奇怪的行为,继承并不像你想象的那样工作。子 Scope 的属性隐藏(覆盖)了父 Scope 中的同名属性,对子 Scope 属性(表单元素)的更改并不更新父 Scope 属性的值。这个行为实际上不是 AngularJS 特有的,JavaScript 本身的原型链就是这样工作的。开发者通常都没有意识到 ng-repeat,ng-switch,ng-view 和 ng-include 统统都创建了他们新的子 scopes,所以在用到这些 directive 时也经常出问题。

二、解决的办法

解决的办法就是不使用基本数据类型,而在 Model 里永远多加一个点.

使用
<input type="text" ng-model="someObj.prop1">
来替代
<input type="text" ng-model="prop1">

是不是很坑爹?下面这个例子很明确地表达了我所想表达的奇葩现象

app.controller('ParentController',function($scope){ $scope.parentPrimitive = "some primitive" $scope.parentObj = {}; $scope.parentObj.parentProperty = "some value"; }); app.controller('ChildController',function($scope){ $scope.parentPrimitive = "this will NOT modify the parent" $scope.parentObj.parentProperty = "this WILL modify the parent"; });

查看在线演示 DEMO

但是我真的确实十分很非常需要使用 string number 等原始数据类型怎么办呢?2 个方法——

  1. 在子 Scope 中使用$parent.parentPrimitive。 这将阻止子 Scope 创建它自己的属性
  2. 在父 Scope 中定义一个函数,让子 Scope 调用,传递原始数据类型的参数给父亲,从而更新父 Scope 中的属性。(并不总是可行)

三、JavaScript 的原型链继承

吐槽完毕,我们来深入了解一下 JavaScript 的原型链。这很重要,特别是当你从服务器端开发转到前端,你应该会很熟悉经典的 Class 类继承,我们来回顾一下。

假设父类 parentScope 有如下成员属性 aString,aNumber,anArray,anObject,以及 aFunction。子类 childScope 原型继承父类 parentScope,于是我们有:

如果子 Scope 尝试去访问 parentScope 中定义的属性,JavaScript 会先在子 Scope 中查找,如果没有该属性,则找它继承的 scope 去获取属性,如果继承的原型对象 parentScope 中都没有该属性,那么继续在它的原型中寻找,从原型链一直往上直到到达 rootScope。所以,下面的表达式结果都是 ture:

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

假设我们执行下面的语句

= 'child string'

原型链并没有被查询,反而是在 childScope 中增加了一个新属性 aString。这个新属性隐藏(覆盖)了 parentScope 中的同名属性。在下面我们讨论 ng-repeat 和 ng-include 时这个概念很重要。

假设我们执行这个操作:

'22' childScope.'child prop1'

原型链被查询了,因为对象 anArray 和 anObject 在 childScope 中没有找到。它们在 parentScope 中被找到了,并且值被更新。childScope 中没有增加新的属性,也没有任何新的对象被创建。(注:在 JavaScript 中,array 和 function 都是对象)

anArray 100555] childScope.anObject { name: 'Mark''USA' 原型链没有被查询,并且子 Scope 新加入了两个新的对象属性,它们隐藏(覆盖)了 parentScope 中的同名对象属性。

应该可以总结

  • 如果读取 childScope.propertyX,并且 childScope 有属性 propertyX,那么原型链没有被查询。
  • 如果设置 childScope.propertyX,原型链不会被查询。

最后一种情况,

delete childScope.anArray childScope.22 // true

我们从 childScope 删除了属性,则当我们再次访问该属性时,原型链会被查询。删除对象的属性会让来自原型链中的属性浮现出来。

四、AngularJS 的 Scope 继承

  • 创建新的 Scope,并且原型继承:ng-repeat,ng-include,ng-view,ng-controller,directive withscope: true,directive withtransclude: true
  • 创建新的 Scope,但不继承:directive withscope: { ... }。它会创建一个独立 Scope。

注:默认情况下 directive 不创建新 Scope,即默认参数是scope: false

ng-include

假设在我们的 controller 中,

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

HTML 为:

<script type="text/ng-template" id"/tpl1.html">
    input ng-model"myPrimitive">
</script>
div ng-include src"'/tpl1.html'"><div>

"/tpl2.html">
    "myObject.aNumber">
"'/tpl2.html'"><div>

每一个 ng-include 会生成一个子 Scope,每个子 Scope 都继承父 Scope。

输入(比如”77″)到第一个 input 文本框,则子 Scope 将获得一个新的 myPrimitive 属性,覆盖掉父 Scope 的同名属性。这可能和你预想的不一样。

输入(比如”99″)到第二个 input 文本框,并不会在子 Scope 创建新的属性,因为 tpl2.html 将 model 绑定到了一个对象属性(an object property),原型继承在这时发挥了作用,ngModel 寻找对象 myObject 并且在它的父 Scope 中找到了。

如果我们不想把 model 从 number 基础类型改为对象,我们可以用 $parent 改写第一个模板:

"$parent.myPrimitive">

输入(比如”22″)到这个文本框也不会创建新属性了。model 被绑定到了父 scope 的属性上(因为 $parent 是子 Scope 指向它的父 Scope 的一个属性)。

对于所有的 scope (原型继承的或者非继承的),Angular 总是会通过 Scope 的 $parent,$$childHead 和 $$childTail 属性记录父-子关系(也就是继承关系),图中为简化而未画出这些属性。

在没有表单元素的情况下,另一种方法是在父 Scope 中定义一个函数来修改基本数据类型。因为有原型继承,子 Scope 确保能够调用这个函数。例如,

// 父 Scope 中 $scope.setMyPrimitive (value{ $scope.= value 查看 DEMO。参考StackOverflow

ng-switch

ng-switch 的原型继承和 ng-include 一样。所以如果你需要对基本类型数据进行双向绑定,使用 $parent,或者将其改为 object 对象并绑定到对象的属性,防止子 Scope 覆盖父 Scope 的属性

参考AngularJS,bind scope of a switch-case?

ng-repeat

ng-repeat 有一点不一样。假设在我们的 controller 里:

myArrayOfPrimitives [ 1122 ]myArrayOfObjects [{num101202]

还有 HTML:

ul> li ng-repeat"num in myArrayOfPrimitives"> "num"> li> ul> "obj in myArrayOfObjects"> "obj.num"> ul>

对于每一个 Item,ng-repeat 创建新的 Scope,每一个 Scope 都继承父 Scope,但同时 item 的值也被赋给了新 Scope 的新属性(新属性的名字为循环的变量名)。Angular ng-repeat 的源码实际上是这样的:

childScope = scope.$new; // 子 scope 原型继承父 scope ... childScope[valueIdent// 创建新的 childScope 属性

如果 item 是一个基础数据类型(就像 myArrayOfPrimitives),本质上它的值被复制了一份赋给了新的子 scope 属性。改变这个子 scope 属性值(比如用 ng-model,即num)不会改变父 scope 引用的 array。所以上面第一个 ng-repeat 里每一个子 scope 获得的num属性独立于 myArrayOfPrimitives 数组:

这样的 ng-repeat 和你预想中的不一样。在 Angular 1.0.2 及更早的版本,向文本框中输入会改变灰色格子的值,它们只在子 Scope 中可见。Angular 1.0.3+ 以后,输入文本不会再有任何作用了。(参考StackOverflow 上的解释)我们希望的是输入能改变 myArrayOfPrimitives 数组,而不是子 Scope 里的属性。为此我们必须将 model 改为一个关于对象的数组(array of objects)。

所以如果 item 是一个对象,则对于原对象的一个引用(而非拷贝)被赋给了新的子 Scope 属性。改变子 Scope 属性的值(使用 ng-model,即 obj.num)也就改变了父 Scope 所引用的对象。所以上面第二个 ng-repeat 可表示为:

这才是我们想要的。输入到文本框即会改变灰色格子的值,该值在父 Scope 和子 Scope 均可见。

参考Difficulty with ng-model,ng-repeat,and inputs以及ng-repeat and databinding

ng-controller

使用 ng-controller 进行嵌套,结果和 ng-include 和 ng-switch 一样是正常的原型继承。所以做法也一样不再赘述。然而“两个 controller 使用 $scope 继承来共享信息被认为是不好的做法”(来自这里),应该使用 service 在 controller 间共享数据。

如果你确实要通过继承来共享数据,那么也没什么特殊要做的,子 Scope 可以直接访问所有父 Scope 的属性。参考Controller load order differs when loading or navigating

directives

这个要分情况来讨论。

  1. 默认scope: false– directive 不会创建新的 Scope,所以没有原型继承。这看上去很简单,但也很危险,因为你会以为 directive 在 Scope 中创建了一个新的属性,而实际上它只是用到了一个已存在的属性。这对编写可复用的模块和组件来说并不好。
  2. scope: true– 这时 directive 会创建一个新的子 scope 并继承父 scope。如果在同一个 DOM 节点上有多个 directive 都要创建新 scope,则只有一个新 Scope 会创建。因为有正常的原型继承,所以和 ng-include, ng-switch 一样要注意基础类型数据的双向绑定,子 Scope 属性会覆盖父 Scope 同名属性
  3. scope: { ... }– 这时 directive 创建一个独立的 scope,没有原型继承。这在编写可复用的模块和组件时是比较好的选择,因为 directive 不会不小心读写父 scope。然而,有时候这类 directives 又经常需要访问父 scope 的属性。对象散列(object hash)被用来建立这个独立 Scope 与父 Scope 间的双向绑定(使用 ‘=’)或单向绑定(使用 ‘@’)。还有一个 ‘&’ 用来绑定父 Scope 的表达式。这些统统从父 Scope 派生创建出本地的 Scope 属性。注意,HTML 属性被用来建立绑定,你无法在对象散列中引用父 Scope 的属性名,你必须使用一个 HTML 属性。例如,<div my-directive>scope: { localProp: '@parentProp' }是无法绑定父属性 parentProp 到独立 scope的,你必须这样指定:<div my-directive the-Parent-Prop=parentProp>以及scope: { localProp: '@theParentProp' }。独立的 scope 中__proto__引用了一个 Scope 对象(下图中的桔黄色 Object),独立 scope 的 $parent 指向父 scope,所以尽管它是独立的而且没有从父 Scope 原型继承,它仍然是一个子 scope。

    下面的图中,我们有<my-directive interpolated="{{parentProp1}}" twowayBinding="parentProp2">scope: { interpolatedProp: '@interpolated',twowayBindingProp: '=twowayBinding' }
    同时,假设 directive 在它的 link 函数里做了scope.someIsolateProp = "I'm isolated"



    注意:在 link 函数中使用attrs.$observe('attr_name',function(value) { ... }获取独立 Scope 用 ‘@’ 符号替换的属性值。例如,在 link 函数中有attrs.$observe('interpolated',function(value) { ... }值将被设为 11. (scope.interpolatedProp在 link 函数中是 undefined,相反scope.twowayBindingProp在 link 函数中定义了,因为用了 ‘=’ 符号)
    更多参考http://onehungrymind.com/angularjs-sticky-notes-pt-2-isolated-scope/
  4. transclude: true– 这时 directive 创建了一个新的 “transcluded” 子 scope,同时继承父 scope。所以如果模板片段中的内容(例如那些将要替代 ng-transclude 的内容)要求对父 Scope 的基本类型数据进行双向绑定,使用 $parent,或者将 model 一个对象的属性,防止子 Scope 属性覆盖父 Scope 属性

    transcluded 和独立 scope (如果有)是兄弟关系,每个 Scope 的 $parent 指向同一个父 Scope。当模板中的 scope 和独立 Scope 同时存在,独立 Scope 属性 $$nextSibling 将会指向模板中的 Scope。
    更多关于 transcluded scope 的信息,参考AngularJS two way binding not working in directive with transcluded scope


    在下图中,假设 directive 和上个图一样,只是多了transclude: true


    查看在线 DEMO,例子里有一个 showScope() 函数可以用来检查独立 Scope 和它关联的 transcluded scope。

总结

一共有四种 Scope:

  1. 普通进行原型继承的 Scope —— ng-include,directive withscope: true
  2. 普通原型继承的 Scope 但拷贝赋值 —— ng-repeat。 每个 ng-repeat 的循环都创建新的子 Scope,并且子 Scope 总是获得新的属性
  3. 独立的 isolate scope —— directive withscope: {...}。它不是原型继承,但 ‘=’,‘@’ 和 ‘&’ 提供了访问父 Scope 属性的机制。
  4. transcluded scope —— directive withtransclude: true。它也遵循原型继承,但它同时是任何 isolate scope 的兄弟。

对于所有的 Scope,Angular 总是会通过 Scope 的 $parent,$$childHead 和 $$childTail 属性记录父-子关系。

参考链接
http://stackoverflow.com/questions/14049480/what-are-the-nuances-of-scope-prototypal-prototypical-inheritance-in-angularjs

原文链接:https://www.f2er.com/angularjs/148884.html

猜你在找的Angularjs相关文章