05作用域-AngularJS基础教程

0. 目录

1. 前言

AngularJS是为了克服HTML在构建应用上的不足而设计的。HTML是一门很好的为静态文本展示设计的声明式语言,但要构建WEB应用的话它就显得乏力了,所以AngularJS做了一些工作来来解决静态网页技术在构建动态应用上的不足。

AngularJS的初衷是为了简化web APP开发,精髓是简单。但是国内外有很多AngularJS的教程,都着重讲AngularJS的强大之处,从而增加了AngularJS学习的难度,本教程试图用通俗的语言讲解最为基础最为实用的内容,简化学习进程、降低学习难度是本教程的初衷。

本系列教程以翻译Chris SmithAngualr Basics为梗概,融合博主自己的理解,为大家提供一个简单明了的学习教程。

本文为系列教程第5篇作用域,翻译自Scope

  1. 引言-Introduction
  2. 基础-Basics
  3. 控制器-Controllers
  4. 作用域-Scopes
  5. 集合-Collections
  6. 模块-Modules
  7. 依赖注入-Dependency Injection
  8. 服务-Services
  9. 过滤器-Filters
  10. 指令-Directives
  11. 指令作用域-Directive Scopes
  12. 路由-Routing
  13. 通信-HTTP
  14. 结论

2.正文

在Angular中scope到底是什么?从名字来看,你可能猜测它是表征程序状态的上下文,保护我们不受JS饱受诟病的全局作用域污染。听起来很简单,我们好像可以直接跳到下一章了。

不要太急,本章虽说不长,但是会涉及一些非常重要的内容:scope继承和体系。一个经典的Angular应用可能会创建10几个、上百个甚至近千个scope,这些scope形成一个体系。

在我们更进一步之前,让我们先搭建本章案例的环境。

2.1 setup

基础章,我们学会了通过向html元素添加ng-app指令告诉Angular需要处理那部分文档。

<!-- index.html -->
<body ng-app="app">
  <!-- Other examples to be inserted here. -->
</body>

ng-app指令的参数就是我们应用的根模块(本案例命名为app,只是为了简单起见)。Angular的模块我们会在下一张详细介绍。现在,我们只需考虑这个启动样板文件(也可以先暂时忽略掉)。

/* module.js */
angular.module(‘app‘, []);
angular.module(‘app‘).config([‘$controllerProvider‘, function($controllerProvider) {
  $controllerProvider.allowGlobals();
}]);

现在让我们把这些放到一边,来点实用的。

2.2 $scope

上一章,我们学习了如何在$scope引用上附加属性的方式准备模型,让我们重复下练习。

/* name-controller.js */
function NameController($scope) {
  $scope.name = "First";
}

使用ng-controller指令,我们可以在DOM元素环境中调用控制器函数。我们在控制器中指定给scope的任意数据都在p元素内可用。

<!-- name-controller.html -->
<p ng-controller="NameController">
  {{name}}
</p>

编译结果为: First

我们调用NameController元素之外的元素,不能访问该控制器的scope。我们来测试一下。

<!-- name-controller-nested.html -->
<div>
  <p>
    Outside the scope: {{name}}
  </p>
  <div ng-controller="NameController">
    <p>
      Inside the scope: {{name}}
    </p>
  </div>
</div>

编译结果为:
Outside the scope:
Inside the scope: First

现在我们明白了scope和controller相匹配。接下来,我们来看下相反的情况:只有一个scope的Angular应用。

2.3 $rootScope

为了帮助我们避免scope使用中可能出现的麻烦,Angular为每一个控制器创建一个新的scope。但是,scope是层次结构的,在每个应用scope层次结构最底端的根scope是单一的。我们可以通过在控制器中声明特定的参数$rootScope访问它,最好不要使用普通的$scope引用。

/* root-name-controller.js */
function RootNameController($rootScope) {
  $rootScope.name = "First";
}

看起来有点天真,实际上它可以正常工作。

<!-- root-name-controller.html -->
<p ng-controller="RootNameController">
  {{name}}
</p>

编译结果为:First.

但是,当我们在另一个控制器里给它指定相同的属性时,就出问题了。

/* second-root-name-controller.js */
function SecondRootNameController($rootScope) {
  $rootScope.name = "Second";
}

这个不对,因为$rootScope在我们的应用中作为单体存在,所以我们只能给它指定一个值。

<!-- root-name-controllers.html -->
<p ng-controller="RootNameController">
  {{name}}
</p>
<p ng-controller="SecondRootNameController">
  {{name}}
</p>

编译结果为:
Second
Second

实际上,我们的SecondRootNameController中指定的name值覆盖了RootNameController中的值,所以这个问题就是全局变量的问题,是吧?

2.4 Isolation(隔离)

通过自动为每个控制器,Angular可以为我们提供非常安全的环境,让我们重新控制器使用正确的方式发布模型,弃用$rootscope而使用$scope注意一点,上文我们的$rootscope的案例仅仅是为了演示为什么每一个控制器自动生成一个scope对象。

/* second-name-controller.js */
function SecondNameController($scope) {
  $scope.name = "Second";
}

使用本章开头的NameController和上面的SecondNameController,我们来演示下$scope对象的隔离性。

<!-- second-name-controller.html -->
<p ng-controller="NameController">
  {{name}}
</p>
<p ng-controller="SecondNameController">
  {{name}}
</p>

编译结果为:
First
Second

该案例产生了正确的结果,每个控制器输出了自己的名字。当一个控制器不是另一个控制器的子元素,产生了隔离性(isolation)。如果是嵌套的情况,又会发生什么呢?

2.5 Nesting(嵌套)

我们稍微修改上面的案例,把第二个控制器SecondNameController移到加载NameController的div元素的子元素上,来看看嵌套的时候会发生什么。

<!-- nested-second-name-controller.html -->
<div ng-controller="NameController">
  <p>
    {{name}}
  </p>
  <p ng-controller="SecondNameController">
    {{name}}
  </p>
</div>

编译结果为:
First
Second

依然能够正常工作。如果我们改变两个p的位置会怎么样,不妨尝试一下,输出结果也会翻转一下,是吧。

好像看起来嵌套的控制器依然可以相互隔离,但其实是个误导。实际上,Angular基于DOM中的相对位置组织控制器的层次结构,嵌套的控制器会继承祖先的属性。我们这里没有变化的原因是,子scope的name属性覆盖了父scope的name属性。

让我们改变一下子scope的属性名称,来试试看scope嵌套中的继承特性。

/* child-controller.js */
function ChildController($scope) {
  $scope.childName = "Child";
}

我们在父元素和子元素中分别使用两个属性。

<-- child-controller.html -->
<div ng-controller="NameController">
  <p>
    {{name}} and {{childName}}
  </p>
  <p ng-controller="ChildController">
    {{name}} and {{childName}}
  </p>
</div>

编译结果为:
First and
First and Child

这样就很明了了,父控制器不能访问子控制器的属性,但是子控制器可以访问自己的属性和父控制器的属性。

因为name是继承的属性,好像我们可以在父scope和子scope中同时看到它的变化,事实是不是这样?我们给父控制器绑定个input来测试下。

<!-- child-controller-edit.html -->
<div ng-controller="NameController">
  <p>
    {{name}}
  </p>
  <p ng-controller="ChildController">
    {{name}}
  </p>
  <input type=‘text‘ ng-model=‘name‘>
</div>

编译结果如下动图所示。
技术分享

如果您尝试编辑name属性,您将会看到期望的结果,如上动图所示。name属性会在两个scope环境内更新。但是,注意我们这里把name绑定在父scope上。

2.6 Inheritance(继承)

您可能觉得我们修改子scope属性会同步更新到父scope中,是这样的吗?让我们测试下,在两个控制器中增加input。

在下面的案例中,第一步修改头一个input,第二个修改第二个input,看看结果是不是你想的那样。

<!-- child-controller-input.html -->
<div ng-controller="NameController">
  <p>
    name: {{name}}
    <br>
    <input type=‘text‘ ng-model=‘name‘>
  </p>
  <p ng-controller="ChildController">
    name: {{name}}
    <br>
    <input type=‘text‘ ng-model=‘name‘>
  </p>
</div>

编译结果如下动图所示。
技术分享

Angular使用JS的普通原型继承,某种程度上来说不错,因为我们无需学习新的东西,某种程度上又不好,因为JS的原型继承不太直观。

对JS对象设置属性导致对象该属性的自动生成,对于属性继承来说不是好消息,因为它将覆盖对象自己的属性。

哈!简单的说,如果您没有修改第二个input,子scope中没有name属性。一旦您修改了input,自动在子scope建立name属性,赋input的值,并且阻断对父scope中name属性的访问。

明白了吗?不明白的话,可以稍微休息下,去学习下原型继承。如果明白了,我们继续。

如果我们想在继承的scope中修改模型数据,我们该如何做?

很简单,我们只需要把name属性移动另一个对象上。

/* info-controller.js */
function InfoController($scope) {
  $scope.info = {name: "First"};
}
/* child-info-controller.js */
function ChildInfoController($scope) {
  $scope.info.childName = "Child";
}
<!-- info-controller.html -->
<div ng-controller="InfoController">
  <p>
    {{info.name}} and {{info.childName}}
    <br>
    <input type=‘text‘ ng-model=‘info.name‘>
  </p>
  <p ng-controller="ChildInfoController">
    {{info.name}} and {{info.childName}}
    <br>
    <input type=‘text‘ ng-model=‘info.name‘>
  </p>
</div>

编译之后结果动图所示。
技术分享

注意ChildInfoController依赖于它的父控制器创建info对象。 如果您编辑ChildInfoController的源文件,把函数体替换为$scope.info = {childName: "Second"};会发生什么呢?尝试一下。

function InfoController($scope) {
  $scope.info = {name: "First"};
}
function ChildInfoController($scope) {
  $scope.info = {childName: "Second"};
}
<div ng-controller="InfoController">
  <p>
    {{info.name}} and {{info.childName}}
    <br>
    <input type=‘text‘ ng-model=‘info.name‘>
  </p>
  <p ng-controller="ChildInfoController">
    {{info.name}} and {{info.childName}}
    <br>
    <input type=‘text‘ ng-model=‘info.name‘>
  </p>
</div>

技术分享

2.7 scope.$watch

大部分时候,Angular的双向绑定可以像您期望的那样完成交互: 当您使用input发生改变时,UI也会同步的发生更改。但是,计算属性(指从其它scope数据中拿到的数据),例如下面案例中所示的sum就是一个计算属性,不是那么一回事儿。

/* sum-controller.js */
function SumController($scope) {
  $scope.values = [1,2];
  $scope.newValue = 1;
  $scope.add = function() {
    $scope.values.push(parseInt($scope.newValue));
  };

  // Broken -- doesn‘t trigger UI update
  $scope.sum = $scope.values.reduce(function(a, b) {
    return a + b;
  });
}

本例的模板文件中,如下所示,我们使用select表单让用户选择数据(1,2,3)添加到数组的末尾。(顺便说一句,控制器给新的值提供了初始值,否则Angular应该为select元素添加空白option以避免武断地给新值赋予第一个option。这个行为对scope用处不大,但是我们有必要了解一下)

<!-- sum-controller.html -->
<p ng-controller="SumController">
  <select ng-model="newValue" ng-options="n for n in [1,2,3]"></select>
  <input type="button" value="Add" ng-click="add()">
  The sum of {{values}} is {{sum}}.
</p>

技术分享

单击add按钮可以把数字添加到数组里去,但是,悲催的是,没有正确的反应到sum里去。

让我们来做点修正,把计算值得部分移到一个回调函数里去。将这个回调函数(具备一个watchExpression参数,本案例就是要被计算的属性)作为参数传递给$scope.$watch,当属性发生改变时调用计算函数求和。

/* sum-watch-controller.js */
function SumController($scope) {
  $scope.values = [1,2];
  $scope.newValue = 1;
  $scope.add = function() {
    $scope.values.push(parseInt($scope.newValue));
  };
  $scope.$watch(‘values‘, function () {
    $scope.sum = $scope.values.reduce(function(a, b) {
      return a + b;
    });
  }, true);
}

技术分享
可以看到,求和的函数就会随着变量的变化发生作用了。

2.8 scope.$apply

Angular内置的双向绑定指令已经很牛了,但是,我们也经常时不时的有些需要添加的行为。比如,如果您想让用户通过esc键同时清除文本框当前状态和scope中的绑定状态。我们应该如何编写这个自定义事件?

<!-- escape-controller.html -->
<div ng-controller="EscapeController">
  <input type="text" ng-model="message">
  is bound to
  "<strong ng-bind="message"></strong>".
  Press <code>esc</code> to clear it!
</div>

首先,我们需要在控制器中声明一个特定名字的参数$element,以便于Angular建立一个关联DOM的引用。使用给定元素的bind方法,我们可以注册一个回调函数,侦听keyup事件。在该回调函数中,我们更新scope属性。简单吧,我们试一下,试着输入点东西,然后按esc键。

/* escape-controller.js */
function EscapeController($scope, $element) {
  $scope.message = ‘‘;
  $element.bind(‘keyup‘, function (event) {
    if (event.keyCode === 27) { // esc key

      // Broken -- doesn‘t trigger UI update
      $scope.message = ‘‘;
    }
  });
}

没有正常工作吧。因为我们这里使用Dom,我们需要告诉Angular什么时候重新渲染视图。我们把需要改变的内容打包成一个回调函数传递给$scope.$apply

/* escape-apply-controller.js */
function EscapeController($scope, $element) {
  $scope.message = ‘‘;
  $element.bind(‘keyup‘, function (event) {
    if (event.keyCode === 27) { // esc key

      $scope.$apply(function() {
        $scope.message = ‘‘;
      });
    }
  });
}

技术分享

这样就可以正常工作了吧。

2.9 结论

如果您试图让Angular适应于MVC模式,scopes会是一个难题。刚开始非常简单,Scopes是模型层的一部分,在Angular中,一个对象直到能够作为scope的属性可以访问时,才成为模型。但是,当您研究scopes如何通过控制器或者指令绑定到DOM时,这些东西将变得很有意思(我们后头再说)。幸运的是,排除这些学术问题外,如上面案例所示,scopes非常直观、简单易用。

3.声明

前端开发whqet,关注前端开发,分享相关资源。csdn专家博客,王海庆希望能对您有所帮助,限于作者水平有限,出错难免,欢迎拍砖!
欢迎任何形式的转载,烦请注明装载,保留本段文字。
本文原文链接,http://blog.csdn.net/whqet/article/details/44925881
欢迎大家访问独立博客http://whqet.github.io

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。