이 기사의 예에서는 AngularJS의 범위, 상속 구조, 이벤트 시스템 및 수명 주기를 설명합니다. 참고할 수 있도록 모든 사람과 공유하세요. 세부 사항은 다음과 같습니다.
Scope 범위에 대한 심층 토론
모든 $scope는 Scope 클래스의 인스턴스입니다. 클래스 범위에는 범위 수명 주기를 제어할 수 있는 메서드가 있고 이벤트 전파 기능을 제공하며 템플릿 렌더링을 지원합니다.
범위 계층
이 간단한 HelloCtrl 예제를 다시 살펴보겠습니다.
var HelloCtrl = function($scope){ $scope.name = 'World'; }
HelloCtrl은 평범한 것처럼 보입니다. 실제로 $scope 매개변수를 제외하고는 실제로 새로운 것이 없습니다. 그런데 이 매개변수는 어디서 오는 걸까요?
이 새로운 범위는 Scope.$new() 메서드를 사용하는 ng-controller 지시문에 의해 생성됩니다. 잠깐, 새 범위를 생성하려면 범위 인스턴스가 하나 이상 있어야 합니다. 예, AngularJS에는 실제로 $rootScope(다른 모든 범위의 상위 항목)가 있습니다. 이 $rootScope 인스턴스는 새 애플리케이션이 시작될 때 생성됩니다.
ng-controller 지시어는 범위를 생성할 수 있는 지시어 중 하나입니다. AngularJS는 DOM 트리에서 이 create-scope 지시어를 만날 때마다 Scope 클래스의 새 인스턴스를 생성합니다. 새로 생성된 범위는 $parent 속성을 통해 자체 상위 범위를 가리킵니다. DOM 트리에는 범위를 생성할 수 있는 지시문이 많이 있으므로 결과적으로 많은 범위가 생성됩니다.
범위의 형태는 부모-자식, 트리형 관계와 유사하며 루트는 $rootScope 인스턴스입니다. 범위 생성이 DOM 트리에 의해 주도되는 것처럼 범위 트리도 DOM의 구조를 모방합니다.
이제 일부 지시문이 새로운 하위 범위를 생성한다는 사실을 알았으므로 이 모든 복잡성이 왜 필요한지 궁금할 것입니다. 이를 이해하기 위해 ng-repeat 루프 지시문이 사용되는 예를 보여드리겠습니다.
컨트롤러는 다음과 같습니다.
var WorldCtrl = function ($scope) { $scope.population = 7000; $scope.countries = [ {name: 'France', population: 63.1}, {name: 'United Kingdom', population: 61.8}, ]; };
템플릿은 다음과 같습니다.
<ul ng-controller="WorldCtrl"> <li ng-repeat="country in countries"> {{country.name}} has population of {{country.population}} </li> <hr> World's population: {{population}} millions </ul>
ng-repeat 지시문은 여러 국가 컬렉션을 반복하고 컬렉션의 각 항목에 대해 새로운 DOM 요소를 생성합니다. ng-repeat 지시문의 구문은 이해하기 매우 쉽습니다. 각 항목에는 새로운 변수 country가 필요하며 뷰 렌더링을 위해 $scope에 매달립니다.
하지만 여기에 문제가 있습니다. 즉, 각 국가마다 $scope에 새 변수를 마운트해야 하며, 이전에 마운트된 값을 단순히 덮어쓸 수는 없습니다. AngularJS는 컬렉션의 각 요소에 대해 새로운 범위를 생성하여 이 문제를 해결합니다. 새로 생성된 이 범위는 일치하는 DOM 트리 구조와 매우 유사하며, 앞서 언급한 멋진 Chrome 확장 프로그램인 Batarang을 사용하여 시각화할 수도 있습니다.
직사각형으로 둘러싸인 각 범위는 자체 데이터 모델을 유지합니다. 동일한 이름을 가진 변수를 다른 범위에 추가하는 데 전혀 문제가 없으며 이름 지정 충돌이 발생하지 않습니다(다른 DOM 요소는 다른 범위를 가리키며 해당 범위의 변수를 사용하여 템플릿을 렌더링합니다). 이러한 방식으로 각 요소에는 고유한 네임스페이스가 있습니다. 이전 예에서 각
범위의 계층 구조 및 상속
범위에 정의된 속성은 해당 하위 항목에 표시됩니다. 하위 범위는 동일한 이름을 가진 속성을 반복적으로 정의할 필요가 없습니다. 이는 범위 체인을 통해 사용할 수 있는 속성을 반복해서 다시 정의할 필요가 없기 때문에 실제로 매우 유용합니다.
이전 예를 다시 살펴보면, 이러한 국가를 전체 세계 인구의 백분율로 표시한다고 가정해 보겠습니다. 이 기능을 구현하려면 다음과 같이 범위에 worldsPercentage 메서드를 정의하고 WorldCtrl로 관리할 수 있습니다.
$scope.worldsPercentage = function (countryPopulation) { return (countryPopulation / $scope.population)*100; }
이 메서드는 다음과 같습니다. ng-repeat에 의해 생성된 모든 범위 인스턴스에 대해 다음과 같이 호출됩니다.
<li ng-repeat="country in countries"> {{country.name}} has population of {{country.population}}, {{worldsPercentage(country.population)}} % of the World's population </li>
AngularJS의 범위 상속 규칙과 JavaScript의 프로토타입 상속 규칙 동일합니다(속성을 읽어야 할 경우 해당 속성을 찾을 때까지 상속 트리를 계속 검색합니다).
스코프 체인을 통한 상속 위험
스코프 계층 관계를 통한 이러한 상속은 데이터를 읽을 때 매우 직관적이고 이해하기 쉽습니다. 하지만 데이터를 쓰면 조금 복잡해집니다.
자식 범위에 있는지 여부에 관계없이 범위에 변수를 정의하는 경우를 살펴보겠습니다. 자바스크립트 코드는
var HelloCtrl = function ($scope) { };
뷰 코드는
<body ng-app ng-init="name='World'"> <h1>Hello, {{name}}</h1> <div ng-controller="HelloCtrl"> Say hello to: <input type="text" ng-model="name"> <h2>Hello, {{name}}!</h2> </div> </body>
이 코드를 실행하면 이 이름 변수가 최상위 범위에서만 정의되지만 전체 애플리케이션에서 볼 수 있다는 것을 알 수 있습니다! 이는 변수가 범위 체인에서 상속되었음을 보여줍니다. 즉, 변수는 상위 범위에서 정의된 다음 하위 범위에서 액세스됩니다.
现在,我们一起来看看,如果在 d5fd7aea971a85678ba271703566ebfd 中写点字会发生什么,运行结果你可能会感到吃惊,因为 HelloCtrl 控制器所初始化的作用域创建了一个新的变量,而不是直接去修改$rootScope 实例中的值。不过当我们认识到作用域也只不过是在彼此间进行了原型继承,也就不会觉得那么吃惊了。所有可以用在 JavaScript 对象上的原型继承的规则,都可以同等的用在 作用域 的原型链继承上去。毕竟 Scopes 作用域就是 JavaScript 对象嘛。
在子级作用域中去改变父级作用域上面的属性有几种方法。第一种,我们就直接通过 $parent 属性来引用父级作用域,但我们要看到,这是一个非常不可靠的解决方案。麻烦之处就在于,ng-model 指令所使用的表达式非常严重的依赖于整个DOM结构。比如就在 d5fd7aea971a85678ba271703566ebfd 标签上面的哪里插入另一个 可创建作用域 的指令,那$parent 就会指向一个完全不同的作用域了。
就经验来讲,尽量避免使用 $parent 属性,因为它强制的把 AngularJS 表达式和你的模板所创建的 DOM 结构捆绑在了一起。这样一来,HTML结构的一个小小的改动,都可能会让整个应用崩溃。
另一个解决方案就是,不要直接把属性绑定到 作用域上,而是绑到一个对象上面,如下所示:
<body ng-app ng-init="thing = {name : 'World'}"> <h1>Hello, {{thing.name}}</h1> <div ng-controller="HelloCtrl"> Say hello to: <input type="text" ng-model="thing.name"> <h2>Hello, {{thing.name}}!</h2> </div> </body>
这个方案会更可靠,因为他并没有假设 DOM 树的结构是什么样子。
避免直接把数据绑定到 作用域的属性上。应优先选择把数据双向绑定到对象的属性上(然后再把对象挂到 scope 上)。就经验而言,在给 ng-model 指令的表达式中,你应该有一个点(例如, ng-model="thing.name")。
作用域层级和事件系统
层级关系中的作用域可以使用 event bus(一种事件系统)。AngularJS可以在作用域层级中传播具名的装备齐全的事件。事件可以从任何一个作用域中发出,然后向上($emit)和向下($broadcast)四处传播。
AngularJS核心服务和指令使用这种事件巴士来发出一些应用程序状态变化的重要事件。比如,我们可以监听$locationChangeSuccess 事件(由 $rootScope 实例发出),然后在任何 location(浏览器中就是URL)变化的时候都会得到通知,如下所示:
$scope.$on('$locationChangeSuccess', function(event, newUrl, oldUrl){ //react on the location change here //for example, update breadcrumbs based on the newUrl });
每一个作用域对象都会有这个 $on 方法,可以用来注册一个作用域事件的侦听器。这个函数所扮演的侦听器在被调用时会有一个 event 对象作为第一个参数。后面的参数会根据事件类型的不同与事件本身的配备一一对应。
类似于 DOM 事件,我们可以调用 event 对象的 preventDefault() 和 stopPropagation() 方法。stopPropagation() 方法将会阻止事件沿着作用域层级继续冒泡,并且只在事件向上层传播的时候($emit)才有效。
尽管 AngularJS 的事件系统是模仿了 DOM 的,但两个事件传播系统是完全独立的,没有任何共同之处。
虽然在作用域层级中传播事件对一些问题来说是一种非常优雅方案(特别是对全局的,异步的状态变化来说),但还是要适度使用。通常情况下,可以依靠双向数据绑定来得到一个比较干净的方案。在整个 AngularJS 框架中,一共只发出($emit)了三个事件($includeContentRequested,$includeContentLoaded,$viewContentLoaded)和七个广播($broadcast)($locationChangeStart, $locationChangeSuccess, $routeUpdate, $routeChangeStart,$routeChangeSuccess, $routeChangeError, $destroy)。正如你所看到的,作用域事件使用的非常少,我们应该在发送自定义的事件之前认真的评估一下其他的可选方案(多数会是双向数据绑定)。
千万不要在 AngularJS 中模仿 DOM 的基于事件的编程方式。大多数情况下,你的应用会有更好的架构方式,你也可以在双向数据绑定这条路上深入探索。
作用域的生命周期
作用域需要提供相互隔离的命名空间,避免变量的命名冲突。作用域们都很小,而且被以层级的方式组织起来,对内存使用的管理来说很有帮助。当其中一个作用域不再需要 ,它就可以被销毁了。结果就是,这个作用域所暴露出来的模型和方法就符合的垃圾回收的标准。
新的作用域通常是被 可创建作用域 的指令所生成和销毁的。不过也可以使用 $new() 和 $destroy() 方法来手动的创建和销毁作用域。
希望本文所述对大家AngularJS程序设计有所帮助。