Developer Guide: Scopes: Scope Internals

What is a scope?

A scope is an execution context for expressions. You can think of a scope as a JavaScript object that has an extra set of APIs for registering change listeners and for managing its own life cycle. In Angular's implementation of the model-view-controller design pattern, a scope's properties comprise both the model and the controller methods.

Scope characteristics

Root scope

Every application has a root scope, which is the ancestor of all other scopes.

What is scope used for?

Expressions in the view are evaluated against the current scope. When HTML DOM elements are attached to a scope, expressions in those elements are evaluated against the attached scope.

There are two kinds of expressions:

Scope inheritance

A scope (prototypically) inherits properties from its parent scope. Since a given property may not reside on a child scope, if a property read does not find the property on a scope, the read will recursively check the parent scope, grandparent scope, etc. all the way to the root scope before defaulting to undefined.

directives associated with elements (ngController, ngRepeat, ngInclude, etc.) create new child scopes that inherit properties from the current parent scope. Any code in Angular is free to create a new scope. Whether or not your code does so is an implementation detail of the directive, that is, you can decide when or if this happens. Inheritance typically mimics HTML DOM element nesting, but does not do so with the same granularity.

A property write will always write to the current scope. This means that a write can hide a parent property within the scope it writes to, as shown in the following example.

it('should inherit properties', inject(function($rootScope)) {
  var root = $rootScope;
  var child = root.$new();

  root.name = 'angular';
  expect(child.name).toEqual('angular');
  expect(root.name).toEqual('angular');

  child.name = 'super-heroic framework';
  expect(child.name).toEqual('super-heroic framework');
  expect(root.name).toEqual('angular');
});

Scope life cycle

  1. Creation

  2. Watcher registration

    Watcher registration can happen at any time and on any scope (root or child) via scope.$watch() API.

  3. Model mutation

    For mutations to be properly observed, you should make them only within the execution of the function passed into scope.$apply() call. (Angular apis do this implicitly, so no extra $apply call is needed when doing synchronous work in controllers, or asynchronous work with $http or $defer services.

  4. Mutation observation

    At the end of each $apply call $digest cycle is started on the root scope, which then propagates throughout all child scopes.

    During the $digest cycle, all $watch-ers expressions or functions are checked for model mutation and if a mutation is detected, the $watch-er 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.

    The root scope can't be destroyed via the $destroy API. Instead, it is enough to remove all references from your application to the scope object and garbage collector will do its magic.

Scopes in Angular applications

To understand how Angular applications work, you need to understand how scopes work within an application. This section describes the typical life cycle of an application so you can see how scopes come into play throughout and get a sense of their interactions.

How scopes interact in applications

  1. At application compile time, a root scope is created and is attached to the root <HTML> DOM element.
  2. During the compilation phase, the compiler matches directives against the DOM template. The directives usually fall into one of two categories:
    • 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.
    • Listener directives, such as ngClick, 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.
  3. 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 ngController and ngRepeat, create new child scopes using the $new() method and then attach the child scope to the corresponding DOM element. You can retrieve a scope for any DOM element by using an angular.element(aDomElement).scope() method call.)

Controllers and scopes

Scopes and controllers interact with each other in the following situations: - Controllers use scopes to expose controller methods to templates (see ngController). - Controllers define methods (behavior) that can mutate the model (properties on the scope). - Controllers may register watches on the model. These watches execute immediately after the controller behavior executes, but before the DOM gets updated.

See the controller docs for more information.

Updating scope properties

You can update a scope by calling its $apply() method with an expression or a function as the function argument. However it is typically not necessary to do this explicitly. In most cases, angular intercepts all external events (such as user interactions, XHRs, and timers) and wraps their callbacks into the $apply() method call on the scope object for you at the right time. The only time you might need to call $apply() explicitly is when you create your own custom asynchronous widget or service.

The reason it is unnecessary to call $apply() from within your controller functions when you use built-in angular widgets and services is because your controllers are typically called from within an $apply() call already.

When a user inputs data, angularized widgets invoke $apply() on the current scope and evaluate an angular expression or execute a function on this scope. Afterwards $apply will trigger $digest call on the root scope, to propagate your changes through the entire system, which results in $watch-ers firing and view getting updated. Similarly, when a request to fetch data from a server is made and the response comes back, the data is written into the model (scope) within an $apply, which then pushes updates through to the view and any other dependents.

A widget that creates scopes (such as ngRepeat) via $new, doesn't need to worry about propagating the $digest call from the parent scope to child scopes. This happens automatically.

Scopes in unit-testing

You can create scopes, including the root scope, in tests by having the $rootScope injected into your spec. This allows you to mimic the run-time environment and have full control over the life cycle of the scope so that you can assert correct model transitions. Since these scopes are created outside the normal compilation process, their life cycles must be managed by the test.

Using scopes in unit-testing

The following example demonstrates how the scope life cycle needs to be manually triggered from within the unit-tests.

  // example of a test
  it('should trigger a watcher', inject(function($rootScope) {
    var scope = $rootScope;
    scope.$watch('name', function(name) {
     scope.greeting = 'Hello ' + name + '!';
    });

    scope.name = 'angular';
    // The watch does not fire yet since we have to manually trigger the digest phase.
    expect(scope.greeting).toEqual(undefined);

    // manually trigger digest phase from the test
    scope.$digest();
    expect(scope.greeting).toEqual('Hello Angular!');
  }

Dependency injection in Tests

When you find it necessary to inject your own mocks in your tests, use a scope to override the service instances, as shown in the following example.

it('should allow override of providers', inject(
  function($provide) {
    $provide.value('$location', {mode:'I am a mock'});
  },
  function($location){
    expect($location.mode).toBe('I am a mock');
  }
)};

Related Topics

Related API