Angular之 Scope和 Directive

---------------------------Scope--------------------------------

https://docs.angularjs.org/guide/scope

What are Scopes?

Scope is an object(是一个数据模型) that refers to the application model. It is an execution context(scope是表达式的执行上下文) for expressions. Scopes are arranged in hierarchical structure which mimic the DOM structure(和DOM结构相像的层级结构) of the application. Scopes can watch expressions and propagate events(监控表达式,传播事件).

Scope characteristics(scope特性)

Scopes provide APIs ($watch) to observe model mutations.

Scopes provide APIs ($apply) to propagate any model changes through the system into the view from outside of the "Angular realm"(Angular外面的) (controllers, services, Angular event handlers).

Scopes can be nested to limit access to the properties of application components while providing access to shared model properties. Nested scopes are either "child scopes" or "isolate scopes". A "child scope" (prototypically) inherits properties from its parent scope. An "isolate scope" does not(独立scope并不从父作用域继承属性方法). See isolated scopes for more information.

Scopes provide context against which expressions are evaluated. For example {{username}} expression is meaningless(单独的表达式是没有意义的,除非是在一个上下文中), unless it is evaluated against a specific scope(特定的作用域) which defines the username property.

$rootScope

Every application has a single root scope(只有一个根作用域). All other scopes are descendant scopes of the root scope. Scopes provide separation between the model and the view, via a mechanism for watching the model for changes.

They also provide event emission/broadcast and subscription facility.

Scope as Data-Model

Scope is the glue between application controller and the view. During the template linking phase(在link阶段,指令设置了$watch) the directives set up $watch expressions on the scope. The $watch allows the directives to be notified of property changes, which allows the directive to render the updated value to the DOM.

Both controllers and directives have reference to the scope, but not to each other(控制器和指令都可以指向作用域,但他们彼此之间却不行). This arrangement isolates the controller from the directive as well as from the DOM. This is an important point since it makes the controllers view agnostic, which greatly improves the testing story of the applications.

In the above example notice that the MyController assigns World to the username property of the scope. The scope then notifies the input of the assignment, which then renders the input with username pre-filled. This demonstrates how a controller can write data into the scope.

Similarly the controller can assign behavior to scope as seen by the sayHello method, which is invoked when the user clicks on the 'greet' button. The sayHello method can read the username property and create a greeting property. This demonstrates that the properties on scope update automatically when they are bound to HTML input widgets.

Logically the rendering of {{greeting}} involves:

retrieval of the scope associated with DOM node where {{greeting}} is defined in template. In this example this is the same scope as the scope which was passed into MyController. (We will discuss scope hierarchies later.)

Evaluate the greeting expression against the scope retrieved above, and assign the result to the text of the enclosing DOM element.

You can think of the scope and its properties(scope和它的属性当做渲染的来源) as the data which is used to render the view.(scope里面的数据被用来渲染视图) The scope is the single source-of-truth for all things view related.

Scope Hierarchies

Each Angular application has exactly one root scope, but may have several child scopes(只有一个根作用域,却可能有好几个子作用域).

The application can have multiple scopes, because some directives create new child scopes(有些指令可以生成新的作用域) (refer to directive documentation to see which directives create new scopes). When new scopes are created, they are added as children of their parent scope. This creates a tree structure which parallels the DOM where they're attached.

When Angular evaluates {{name}}, it first looks at the scope associated with the given element for the name property. If no such property is found, it searches the parent scope and so on until the root scope is reached.(一级一级向父级作用域搜寻,直到根作用域) In JavaScript this behavior is known as prototypical inheritance, and child scopes prototypically inherit from their parents.

This example illustrates scopes in application, and prototypical inheritance of properties. The example is followed by a diagram depicting the scope boundaries.

Notice that Angular automatically places(自动添加类) ng-scope class on elements where scopes are attached. The <style> definition in this example highlights in red the new scope locations. The child scopes are necessary because the repeater evaluates {{name}} expression, but depending on which scope the expression is evaluated it produces different result. Similarly the evaluation of {{department}} prototypically inherits from root scope, as it is the only place where the department property is defined.

Retrieving Scopes from the DOM.

Scopes are attached to the DOM as $scope data property, and can be retrieved for debugging purposes. (It is unlikely that one would need to retrieve scopes in this way inside the application.) The location where the root scope is attached to the DOM is defined by the location of ng-app directive. Typically ng-app is placed on the <html> element, but it can be placed on other elements as well, if, for example, only a portion of the view needs to be controlled by Angular.

To examine the scope in the debugger:

Right click on the element of interest in your browser and select 'inspect element'. You should see the browser debugger with the element you clicked on highlighted.

The debugger allows you to access the currently selected element in the console as $0 variable.

To retrieve the associated scope in console execute: angular.element($0).scope() or just type $scope

Scope Events Propagation(scope事件传播)

Scopes can propagate events in similar fashion to DOM events. The event can be broadcasted to the scope children or emitted to scope parents.

Scope Life Cycle

The normal flow of a browser receiving an event is that it executes a corresponding JavaScript callback. Once the callback completes the browser re-renders the DOM and returns to waiting for more events.

When the browser calls into JavaScript the code executes outside the Angular execution context, which means that Angular is unaware of model modifications. To properly process model modifications the execution has to enter the Angular execution context using the $apply method. Only model modifications which execute inside the $apply method will be properly accounted for by Angular. For example if a directive listens on DOM events, such as ng-click it must evaluate the expression inside the $apply method.

After evaluating the expression, the $apply method performs a $digest.($apply会调用起$digest循环,进行脏值检查) In the $digest phase the scope examines all of the $watch expressions and compares them with the previous value. This dirty checking is done asynchronously(脏值检查是异步的,即值变化和检查是“两个”时间段). This means that assignment such as $scope.username="angular" will not immediately cause a $watch to be notified, instead the $watch notification is delayed until the $digest phase. This delay is desirable, since it coalesces multiple model updates into one $watch notification as well as guarantees that during the $watch notification no other $watches are running. If a $watch changes the value of the model, it will force additional $digest cycle.

1 Creation

The root scope is created during the application bootstrap(自启动) by the $injector. During template linking(在link阶段,有些指令会生成自己的作用域), some directives create new child scopes.

2 Watcher registration

During template linking(在linking阶段,注册watch), directives register watches on the scope. These watches will be used to propagate model values to the DOM.

3 Model mutation

For mutations to be properly observed, you should make them only within the scope.$apply(). Angular APIs do this implicitly(angular内置的指令会隐式的这么做), so no extra $apply call is needed when doing synchronous work in controllers, or asynchronous work with $http, $timeout or $interval services.

4 Mutation observation

At the end of $apply, Angular performs a $digest cycle on the root scope, which then propagates throughout all child scopes. During the $digest cycle, all $watched expressions or functions are checked for model mutation and if a mutation is detected, the $watch listener is called.

5 Scope destruction

When child scopes are no longer needed, it is the responsibility of the child scope creator to destroy them via scope.$destroy() API. This will stop propagation of $digest calls into the child scope and allow for memory used by the child scope models to be reclaimed by the garbage collector.

Scopes and Directives

During the compilation phase, the compiler matches directives against the DOM template(compiler把指令和dom模板一一对应链接起来). The directives usually fall into one of two categories:

  1. Observing directives, such as double-curly expressions {{expression}}, register listeners using the $watch() method. This type of directive needs to be notified whenever the expression changes so that it can update the view.
  2.  Listener directives, such as ng-click, register a listener with the DOM. When the DOM listener fires, the directive executes the associated expression and updates the view using the $apply() method.

When an external event (such as a user action, timer or XHR) is received, the associated expression must be applied to the scope through the $apply() method so that all listeners are updated correctly.

Directives that Create Scopes

In most cases, directives and scopes interact but do not create new instances of scope. However, some directives, such as ng-controller and ng-repeat, create new child scopes and attach the child scope to the corresponding DOM element(在相应的dom节点上增加子作用域). You can retrieve a scope for any DOM element by using an angular.element(aDomElement).scope() method call. See the directives guide for more information about isolate scopes.

Controllers and Scopes

Scopes and controllers interact with each other in the following situations:

  1. Controllers use scopes to expose controller methods to templates (see ng-controller).
  2. Controllers define methods (behavior) that can mutate the model (properties on the scope).
  3. Controllers may register watches on the model. These watches execute immediately after the controller behavior executes.

Scope $watch Performance Considerations

Dirty checking the scope for property changes is a common operation in Angular and for this reason the dirty checking function must be efficient(脏值检查一定要效率很高). Care should be taken that the dirty checking function does not do any DOM access(dom操作要比js对象操作慢好多), as DOM access is orders of magnitude slower than property access on JavaScript object.

Scope $watch Depths

Dirty checking can be done with three strategies: By reference, by collection contents, and by value. The strategies differ in the kinds of changes they detect, and in their performance characteristics.

三者的效率由上自下依次降低:

1 Watching by reference (scope.$watch (watchExpression, listener)) detects a change when the whole value returned by the watch expression switches to a new value. If the value is an array or an object, changes inside it are not detected. This is the most efficient strategy.
2 Watching collection contents (scope.$watchCollection (watchExpression, listener)) detects changes that occur inside an array or an object: When items are added, removed, or reordered. The detection is shallow - it does not reach into nested collections(对于嵌套的并不检测). Watching collection contents is more expensive than watching by reference, because copies of the collection contents need to be maintained. However, the strategy attempts to minimize the amount of copying required.
3 Watching by value (scope.$watch (watchExpression, listener, true)) detects any change(检测出来任何变化) in an arbitrarily nested data structure. It is the most powerful change detection strategy, but also the most expensive. A full traversal of the nested data structure is needed on each digest, and a full copy of it needs to be held in memory.

Integration with the browser event loop

The diagram and the example below describe how Angular interacts with the browser's event loop.

The browser's event-loop waits for an event to arrive. An event is a user interaction, timer event, or network event (response from a server).
The event's callback gets executed. This enters the JavaScript context(进入Js的“环境”). The callback can modify the DOM structure.
Once the callback executes, the browser leaves the JavaScript context and re-renders the view based on DOM changes.
Angular modifies the normal JavaScript flow by providing its own event processing loop. This splits the JavaScript into classical and Angular execution context(把js分为经典的js环境和Angular自己的环境). Only operations which are applied in the Angular execution context will benefit from Angular data-binding, exception handling, property watching, etc... You can also use ($apply()) to enter the Angular execution context from JavaScript. Keep in mind that in most places (controllers, services) .$apply has already been called for you by the directive which is handling the event. An explicit call to .$apply is needed only when implementing custom event callbacks, or when working with third-party library callbacks.

Enter the Angular execution context by calling scope.$apply(stimulusFn), where stimulusFn is the work you wish to do in the Angular execution context.

Angular executes the stimulusFn(), which typically modifies application state.
Angular enters the $digest loop. The loop is made up of two smaller loops which process $evalAsync queue and the $watch list. The $digest loop keeps iterating until the model stabilizes, which means that the $evalAsync queue is empty and the $watch list does not detect any changes.
The $evalAsync queue is used to schedule work which needs to occur outside of current stack frame, but before the browser's view render. This is usually done with setTimeout(0), but the setTimeout(0) approach suffers from slowness and may cause view flickering since the browser renders the view after each event.
The $watch list is a set of expressions which may have changed since last iteration. If a change is detected then the $watch function is called which typically updates the DOM with the new value.
Once the Angular $digest loop finishes, the execution leaves the Angular and JavaScript context. This is followed by the browser re-rendering the DOM to reflect any changes.
Here is the explanation of how the Hello world example achieves the data-binding effect when the user enters text into the text field.

During the compilation phase:
the ng-model and input directive set up a keydown listener on the <input> control.
the interpolation sets up a ($watch) to be notified of name changes.
During the runtime phase:
Pressing an 'X' key causes the browser to emit a keydown event on the input control.
The input directive captures the change to the input's value and calls $apply("name = 'X';") to update the application model inside the Angular execution context.
Angular applies the name = 'X'; to the model.
The $digest loop begins
The $watch list detects a change on the name property and notifies the interpolation, which in turn updates the DOM.
Angular exits the execution context, which in turn exits the keydown event and with it the JavaScript execution context.
The browser re-renders the view with the updated text.

 ----------------------Directive-------------------------------

https://docs.angularjs.org/guide/directive

What are Directives?

At a high level, directives are markers on a DOM element (such as an attribute, element name, comment or CSS class) that tell AngularJS's HTML compiler ($compile) to attach a specified behavior to that DOM element (e.g. via event listeners), or even to transform the DOM element and its children.(指令就是dom节点上的标记,这些指令“告诉"Angular编译器来添加一些特定的行为到dom节点)

Angular comes with a set of these directives built-in, like ngBind, ngModel, and ngClass. Much like you create controllers and services, you can create your own directives for Angular to use. When Angular bootstraps your application(自启动应用时), the HTML compiler traverses(遍历) the DOM matching directives against the DOM elements.

What does it mean to "compile" an HTML template? For AngularJS, "compilation" means attaching directives to the HTML to make it interactive.(对Angular来说,”编译“的意思是添加指令到HTML中,使得它interactive) The reason we use the term "compile" is that the recursive process of attaching directives mirrors the process of compiling source code in compiled programming languages.

Matching Directives

Before we can write a directive, we need to know how Angular's HTML compiler determines when to use a given directive.(什么时候用指令)

Similar to the terminology used when an element matches a selector, we say an element matches a directive when the directive is part of its declaration.

In the following example, we say that the <input> element matches the ngModel directive

<input ng-model="foo">
The following <input> element also matches ngModel:

<input data-ng-model="foo">
And the following element matches the person directive:

<person>{{name}}</person>

Normalization

Angular normalizes an element's tag and attribute name to determine which elements match which directives.(Angular”规格化“一个element的tag和属性名字来决定哪一个元素match哪一个指令)

We typically refer to directives by their case-sensitive camelCase normalized name (e.g. ngModel). However, since HTML is case-insensitive, we refer to directives in the DOM by lower-case forms, typically using dash-delimited attributes on DOM elements (e.g. ng-model).

The normalization process is as follows:

  1. Strip(去掉) x- and data- from the front of the element/attributes.(去掉前面的前缀)
  2. Convert the :, -, or _-delimited name to camelCase.(转化为驼峰命名法)

For example, the following forms are all equivalent and match the ngBind directive:

<div ng-controller="Controller">
Hello <input ng-model='name'> <hr/>
<span ng-bind="name"></span> <br/>
<span ng:bind="name"></span> <br/>
<span ng_bind="name"></span> <br/>
<span data-ng-bind="name"></span> <br/>
<span x-ng-bind="name"></span> <br/>
</div>

 Best Practice: Prefer using the dash-delimited format (e.g. ng-bind for ngBind). If you want to use an HTML validating tool, you can instead use the data-prefixed version (e.g. data-ng-bind for ngBind). The other forms shown above are accepted for legacy reasons but we advise you to avoid them.

Directive types

$compile can match directives based on element names, attributes, class names, as well as comments.

All of the Angular-provided directives match attribute name, tag name, comments, or class name. The following demonstrates the various ways a directive (myDir in this case) can be referenced from within a template:

<my-dir></my-dir>
<span my-dir="exp"></span>
<!-- directive: my-dir exp -->
<span class="my-dir: exp;"></span>
Best Practice: Prefer using directives via tag name and attributes over comment and class names. Doing so generally makes it easier to determine what directives a given element matches.
Best Practice: Comment directives were commonly used in places where the DOM API limits the ability to create directives that spanned multiple elements (e.g. inside <table> elements). AngularJS 1.2 introduces ng-repeat-start and ng-repeat-end as a better solution to this problem. Developers are encouraged to use this over custom comment directives when possible.

Creating Directives

First let's talk about the API for registering directives. Much like controllers, directives are registered on modules.(就像控制器,指令也是直接”注册“在模块上面的) To register a directive, you use the module.directive API. module.directive takes the normalized directive name followed by a factory function. This factory function should return an object(这个工厂函数应该返回一个对象) with the different options to tell $compile how the directive should behave when matched.

The factory function is invoked only once when the compiler matches the directive for the first time. You can perform any initialization work here. The function is invoked using $injector.invoke which makes it injectable just like a controller.

We'll go over a few common examples of directives, then dive deep into the different options and compilation process.

Best Practice: In order to avoid collisions with some future standard, it's best to prefix your own directive names. For instance, if you created a <carousel> directive, it would be problematic if HTML7 introduced the same element. A two or three letter prefix (e.g.btfCarousel) works well. Similarly, do not prefix your own directives with ng or they might conflict with directives included in a future version of Angular.

The restrict option is typically set to:

  • 'A' - only matches attribute name
  • 'E' - only matches element name
  • 'C' - only matches class name
  • 'M' - only matches comment

These restrictions can all be combined as needed:

  • 'AEC' - matches either attribute or element or class name

Isolating the Scope of a Directive(独立的作用域)

Our myCustomer directive above is great, but it has a fatal flaw. We can only use it once within a given scope.

In its current implementation, we'd need to create a different controller each time in order to re-use such a directive:(当我们想重复利用指令,就必须create不同的控制器,因此我们需要独立的作用域(则每个指令都会产生自己的作用域,不是控制器所在的这个作用域))

What we want to be able to do is separate the scope inside a directive from the scope outside, and then map the outer scope to a directive's inner scope. We can do this by creating what we call an isolate scope. To do this, we can use a directive's scope option:

Looking at index.html, the first <my-customer> element binds the info attribute to naomi, which we have exposed on our controller's scope. The second binds info to igor.

Let's take a closer look at the scope option:

//...
scope: {
customerInfo: '=info'
},
//...
The scope option is an object that contains a property for each isolate scope binding. In this case it has just one property:

Its name (customerInfo) corresponds to the directive's isolate scope property customerInfo.
Its value (=info) tells $compile to bind to the info attribute.
Note: These =attr attributes in the scope option of directives are normalized just like directive names. To bind to the attribute in <div bind-to-this="thing">, you'd specify a binding of =bindToThis.

For cases where the attribute name is the same as the value you want to bind to inside the directive's scope(属性名和你想绑定的值是一样的话), you can use this shorthand syntax:

...
scope: {
  // same as '=customer'
  customer: '='
},
...

Besides making it possible to bind different data to the scope inside a directive(在同一个控制器内,同一个指令可以绑定不同的值), using an isolated scope has another effect.

Notice that {{vojta.name}} and {{vojta.address}} are empty, meaning they are undefined. Although we defined vojta in the controller, it's not available within the directive.

As the name suggests, the isolate scope of the directive isolates everything(孤立的所有的东西) except models that you've explicitly added to the scope: {} hash object. This is helpful when building reusable components because it prevents a component from changing your model state except for the models that you explicitly pass in.

Note: Normally, a scope prototypically inherits from its parent. An isolated scope does not(正常情况下,一个scope会从父级scope继承属性,但是独立作用域不会). See the "Directive Definition Object - scope" section for more information about isolate scopes.
Best Practice: Use the scope option to create isolate scopes when making components that you want to reuse throughout your app(在应用中重复使用时,就利用独立作用域).

Creating a Directive that Manipulates the DOM

In this example we will build a directive that displays the current time. Once a second, it updates the DOM to reflect the current time.

Directives that want to modify the DOM typically use the link option to register DOM listeners as well as update the DOM.(如果指令想要更新操作dom,可以使用link函数在dom节点上注册监听函数和更新dom节点) It is executed after the template has been cloned and is where directive logic will be put.

link takes a function with the following signature, function link(scope, element, attrs, controller, transcludeFn) { ... }, where:

  • scope is an Angular scope object.
  • element is the jqLite-wrapped element that this directive matches.
  • attrs is a hash object with key-value pairs of normalized attribute names and their corresponding attribute values.
  • controller is the directive's required controller instance(s) or its own controller (if any)(指令自己的控制器). The exact value depends on the directive's require property.
  • transcludeFn is a transclude linking function pre-bound to the correct transclusion scope.

In our link function, we want to update the displayed time once a second, or whenever a user changes the time formatting string that our directive binds to. We will use the $interval service to call a handler on a regular basis. This is easier than using $timeout but also works better with end-to-end testing, where we want to ensure that all $timeouts have completed before completing the test. We also want to remove the $interval if the directive is deleted so we don't introduce a memory leak.

There are a couple of things to note here. Just like the module.controller API, the function argument in module.directive is dependency injected(函数的参数也是依赖注入的). Because of this, we can use $interval and dateFilter inside our directive's link function.

We register an event element.on('$destroy', ...). What fires this $destroy event?

There are a few special events that AngularJS emits. When a DOM node that has been compiled with Angular's compiler is destroyed, it emits a $destroy event. Similarly, when an AngularJS scope is destroyed, it broadcasts a $destroy event to listening scopes.

By listening to this event, you can remove event listeners that might cause memory leaks. Listeners registered to scopes and elements are automatically cleaned up when they are destroyed, but if you registered a listener on a service, or registered a listener on a DOM node that isn't being deleted, you'll have to clean it up yourself or you risk introducing a memory leak.

Best PracticeDirectives should clean up after themselves. You can use element.on('$destroy', ...) orscope.$on('$destroy', ...) to run a clean-up function when the directive is removed.

 

posted on 2016-09-11 22:14  kevin4dev  阅读(916)  评论(0编辑  收藏  举报

导航