All Downloads are FREE. Search and download functionalities are using the official Maven repository.

package.src.core.scope.scope.js Maven / Gradle / Ivy

import {
  concat,
  minErr,
  nextUid,
  isFunction,
  isUndefined,
  isObject,
  isArrayLike,
  isNumberNaN,
  arrayRemove,
  equals,
} from "../../shared/utils";

/**
 * @enum {number}
 */
export const ScopePhase = {
  NONE: 0,
  APPLY: 1,
  DIGEST: 2,
};

/**
 * @typedef {Object} AsyncQueueTask
 * @property {Scope} scope
 * @property {Function} fn
 * @property {Object} locals
 */

/**
 * @typedef {function(any, any, Scope): any} WatchListener
 * Callback function triggered whenever the value of `watchExpression` changes.
 *
 * @param {any} newVal - The current value of the `watchExpression`.
 * @param {any} oldVal - The previous value of the `watchExpression`.
 * @param {Scope} scope - The current scope in which the `watchExpression` is evaluated.
 *
 */

/**
 * @typedef {string | ((scope: Scope) => any)} WatchExpression
 */

/**
 *
 * The default number of `$digest` iterations the scope should attempt to execute before giving up and
 * assuming that the model is unstable. In complex applications it's possible that the dependencies between `$watch`s will result in
 * several digest iterations.
 *
 * @typedef {number} TTL The number of digest iterations
 *
 * @type {TTL}
 */
export const TTL = 10;

const $rootScopeMinErr = minErr("$rootScope");

/** @type {AsyncQueueTask[]} */
export const $$asyncQueue = [];
export const $postUpdateQueue = [];

/**
 * @type {Function[]}
 */
export const $$applyAsyncQueue = [];
let postDigestQueuePosition = 0;
let lastDirtyWatch = null;
let applyAsyncId = null;

/** Services required by each scope instance */
/** @type {import('../parse/parse').ParseService} */
let $parse;
/** @type {import('../../services/browser').Browser} */
let $browser;
/**@type {import('../exception-handler').ErrorHandler} */
let $exceptionHandler;

/**
 * Provider responsible for instantiating the initial scope, aka - root scope.
 * Every application has a single root {@link ng.$rootScope.Scope 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. See the
 * {@link guide/scope developer guide on scopes}.
 *
 * The provider also injects runtime services to make them available to all scopes.
 *
 */
export class RootScopeProvider {
  constructor() {
    this.rootScope = new Scope(true);
  }

  $get = [
    "$exceptionHandler",
    "$parse",
    "$browser",
    /**
     * @param {import('../exception-handler').ErrorHandler} exceptionHandler
     * @param {import('../parse/parse').ParseService} parse
     * @param {import('../../services/browser').Browser} browser
     * @returns {Scope} root scope
     */
    (exceptionHandler, parse, browser) => {
      $exceptionHandler = exceptionHandler;
      $parse = parse;
      $browser = browser;
      return this.rootScope;
    },
  ];
}

/**
 * DESIGN NOTES
 *
 * The design decisions behind the scope are heavily favored for speed and memory consumption.
 *
 * The typical use of scope is to watch the expressions, which most of the time return the same
 * value as last time so we optimize the operation.
 *
 * Closures construction is expensive in terms of speed as well as memory:
 *   - No closures, instead use prototypical inheritance for API
 *   - Internal state needs to be stored on scope directly, which means that private state is
 *     exposed as $$____ properties
 *
 * Loop operations are optimized by using while(count--) { ... }
 *   - This means that in order to keep the same order of execution as addition we have to add
 *     items to the array at the beginning (unshift) instead of at the end (push)
 *
 * Child scopes are created and removed often
 *   - Using an array would be slow since inserts in the middle are expensive; so we use linked lists
 *
 * There are fewer watches than observers. This is why you don't want the observer to be implemented
 * in the same way as watch. Watch requires return of the initialization function which is expensive
 * to construct.
 */

export class Scope {
  /**
   * @param {boolean} [root=false] - Indicates if this scope is the root scope.
   */
  constructor(root = false) {
    /**
     * @type {boolean}
     */
    this.isRoot = root;

    /**
     * @type {number} Unique scope ID (monotonically increasing) useful for debugging.
     */
    this.$id = nextUid();

    /** @type {ScopePhase} */
    this.$$phase = ScopePhase.NONE;

    /**
     * @type {?Scope} Reference to the parent scope.
     */
    this.$parent = null;

    /**
     * @type {?Scope}
     */
    this.$root = this;

    /**
     * @type {Array}
     */
    this.$$watchers = [];

    /**
     * @type {number}
     */
    this.$$digestWatchIndex = -1;

    /**
     * @type {?Scope}
     */
    this.$$nextSibling = null;

    /**
     * @type {?Scope}
     */
    this.$$prevSibling = null;

    /**
     * @type {?Scope}
     */
    this.$$childHead = null;

    /**
     * @type {?Scope}
     */
    this.$$childTail = null;

    /** @type {boolean} */
    this.$$destroyed = false;

    /** @type {boolean} */
    this.$$suspended = false;

    /** @type {Map} */
    this.$$listeners = new Map();

    /** @type {object} */
    this.$$listenerCount = {};

    /** @type {number} */
    this.$$watchersCount = 0;
    this.$$isolateBindings = null;
  }

  /**
   * Creates a new child {@link Scope}.
   *
   * The parent scope will propagate the {@link ng.$rootScope.Scope#$digest $digest()} event.
   * The scope can be removed from the scope hierarchy using {@link ng.$rootScope.Scope#$destroy $destroy()}.
   *
   * {@link ng.$rootScope.Scope#$destroy $destroy()} must be called on a scope when it is
   * desired for the scope and its child scopes to be permanently detached from the parent and
   * thus stop participating in model change detection and listener notification by invoking.
   *
   * @param {?boolean} isolate If true, then the scope does not prototypically inherit from the
   *         parent scope. The scope is isolated, as it can not see parent scope properties.
   *         When creating widgets, it is useful for the widget to not accidentally read parent
   *         state.
   *
   *
   * @returns {Scope} The newly created child scope.
   *
   */
  $new(isolate) {
    let child = isolate ? new Scope() : Object.create(this);

    if (isolate) {
      child.$root = this.$root;
    } else {
      // Initialize properties for a non-isolated child scope
      child.$id = nextUid();
      child.$$watchers = [];
      child.$$nextSibling = null;
      child.$$childHead = null;
      child.$$childTail = null;
      child.$$listeners = new Map();
      child.$$listenerCount = {};
      child.$$watchersCount = 0;
      child.$$suspended = false;
    }

    child.$parent = this;
    child.$$prevSibling = child.$parent.$$childTail;

    if (child.$parent.$$childHead) {
      child.$parent.$$childTail.$$nextSibling = child;
      child.$parent.$$childTail = child;
    } else {
      child.$parent.$$childHead = child;
      child.$parent.$$childTail = child;
    }

    // Add a destroy listener if isolated or the parent differs from `this`
    if (isolate) {
      child.$on("$destroy", ($event) => {
        $event.currentScope.$$destroyed = true;
      });
    }

    return child;
  }

  /**
   * Creates a transcluded scope
   * @param {Scope} parent The {@link ng.$rootScope.Scope `Scope`} that will be the `$parent`
   * of the newly created scope. This is used when creating a transclude scope to correctly place it
   *  in the scope hierarchy while maintaining the correct prototypical inheritance.
   *
   * @returns {Scope} The newly created child scope.
   *
   */
  $transcluded(parent) {
    let child = Object.create(this);

    // Initialize properties for a non-isolated child scope
    child.$id = nextUid();
    child.$$watchers = [];
    child.$$nextSibling = null;
    child.$$childHead = null;
    child.$$childTail = null;
    child.$$listeners = new Map();
    child.$$listenerCount = {};
    child.$$watchersCount = 0;
    child.$$suspended = false;

    child.$parent = parent || this;
    child.$$prevSibling = child.$parent.$$childTail;

    if (child.$parent.$$childHead) {
      child.$parent.$$childTail.$$nextSibling = child;
      child.$parent.$$childTail = child;
    } else {
      child.$parent.$$childHead = child;
      child.$parent.$$childTail = child;
    }

    // Add a destroy listener if isolated or the parent differs from `this`
    if (parent !== this) {
      child.$on("$destroy", ($event) => {
        $event.currentScope.$$destroyed = true;
      });
    }

    return child;
  }
  /**
 * Registers a `listener` callback to be executed whenever the `watchExpression` changes.
 *
 * - The `watchExpression` is called on every call to {@link ng.$rootScope.Scope#$digest
 *   $digest()} and should return the value that will be watched. (`watchExpression` should not change
 *   its value when executed multiple times with the same input because it may be executed multiple
 *   times by {@link ng.$rootScope.Scope#$digest $digest()}. That is, `watchExpression` should be
 *   [idempotent](http://en.wikipedia.org/wiki/Idempotence).)
 * - The `listener` is called only when the value from the current `watchExpression` and the
 *   previous call to `watchExpression` are not equal (with the exception of the initial run,
 *   see below). Inequality is determined according to reference inequality,
 *   [strict comparison](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Comparison_Operators)
 *    via the `!==` Javascript operator, unless `objectEquality == true`
 *   (see next point)
 * - When `objectEquality == true`, inequality of the `watchExpression` is determined
 *   according to the {@link angular.equals} function. To save the value of the object for
 *   later comparison, the {@link structuredClone} function is used. This therefore means that
 *   watching complex objects will have adverse memory and performance implications.
 * - This should not be used to watch for changes in objects that are (or contain)
 *   [File](https://developer.mozilla.org/docs/Web/API/File) objects due to limitations with {@link structuredClone `structuredClone`}.
 * - The watch `listener` may change the model, which may trigger other `listener`s to fire.
 *   This is achieved by rerunning the watchers until no changes are detected. The rerun
 *   iteration limit is 10 to prevent an infinite loop deadlock.
 *
 *
 * If you want to be notified whenever {@link ng.$rootScope.Scope#$digest $digest} is called,
 * you can register a `watchExpression` function with no `listener`. (Be prepared for
 * multiple calls to your `watchExpression` because it will execute multiple times in a
 * single {@link ng.$rootScope.Scope#$digest $digest} cycle if a change is detected.)
 *
 * After a watcher is registered with the scope, the `listener` fn is called asynchronously
 * (via {@link ng.$rootScope.Scope#$evalAsync $evalAsync}) to initialize the
 * watcher. In rare cases, this is undesirable because the listener is called when the result
 * of `watchExpression` didn't change. To detect this scenario within the `listener` fn, you
 * can compare the `newVal` and `oldVal`. If these two values are identical (`===`) then the
 * listener was called due to initialization.
 *
 *
 *
 * @example
 * ```js
     // let's assume that scope was dependency injected as the $rootScope
     let scope = $rootScope;
     scope.name = 'misko';
     scope.counter = 0;

     expect(scope.counter).toEqual(0);
     scope.$watch('name', function(newValue, oldValue) {
       scope.counter = scope.counter + 1;
     });
     expect(scope.counter).toEqual(0);

     ;
     // the listener is always called during the first $digest loop after it was registered
     expect(scope.counter).toEqual(1);

     ;
     // but now it will not be called unless the value changes
     expect(scope.counter).toEqual(1);

     scope.name = 'adam';
     ;
     expect(scope.counter).toEqual(2);

     // Using a function as a watchExpression
     let food;
     scope.foodCounter = 0;
     expect(scope.foodCounter).toEqual(0);
     scope.$watch(
       // This function returns the value being watched. It is called for each turn of the $digest loop
       function() { return food; },
       // This is the change listener, called when the value returned from the above function changes
       function(newValue, oldValue) {
         if ( newValue !== oldValue ) {
           // Only increment the counter if the value changed
           scope.foodCounter = scope.foodCounter + 1;
         }
       }
     );
     // No digest has been run so the counter will be zero
     expect(scope.foodCounter).toEqual(0);

     // Run the digest but since food has not changed count will still be zero
     ;
     expect(scope.foodCounter).toEqual(0);

     // Update food and run digest.  Now the counter will increment
     food = 'cheeseburger';
     ;
     expect(scope.foodCounter).toEqual(1);

 * ```
 *
 *
 *
 * @param {string | ((scope: Scope) => any) | import("../parse/parse.js").CompiledExpression} watchExp Expression that is evaluated on each
 *    {@link ng.$rootScope.Scope#$digest $digest} cycle. A change in the return value triggers
 *    a call to the `listener`.
 *
 *    - `string`: Evaluated as {@link guide/expression expression}
 *    - `function(scope)`: called with current `scope` as a parameter.
 * @param {WatchListener} [listener]
 * @param {boolean=} [objectEquality=false] Compare for object equality using {@link angular.equals} instead of
 *     comparing for reference equality.
 * @returns {function()} Returns a deregistration function for this listener.
 */
  $watch(watchExp, listener, objectEquality) {
    const get = $parse(watchExp);
    const fn = isFunction(listener) ? listener : () => {};

    if (get.$$watchDelegate) {
      return get.$$watchDelegate(this, fn, objectEquality, get, watchExp);
    }

    const watcher = {
      fn,
      last: initWatchVal,
      get,
      exp: watchExp,
      eq: !!objectEquality,
    };

    lastDirtyWatch = null;

    if (this.$$watchers.length === 0) {
      this.$$digestWatchIndex = -1;
    }
    // we use unshift since we use a while loop in $digest for speed.
    // the while loop reads in reverse order.
    this.$$watchers.unshift(watcher);
    this.$$digestWatchIndex++;
    this.incrementWatchersCount(1);

    return () => {
      const index = arrayRemove(this.$$watchers, watcher);
      if (index >= 0) {
        this.incrementWatchersCount(-1);
        if (index < this.$$digestWatchIndex) {
          this.$$digestWatchIndex--;
        }
      }
      lastDirtyWatch = null;
    };
  }

  /**
   * A variant of {@link ng.$rootScope.Scope#$watch $watch()} where it watches an array of `watchExpressions`.
   * If any one expression in the collection changes the `listener` is executed.
   *
   * - The items in the `watchExpressions` array are observed via the standard `$watch` operation. Their return
   *   values are examined for changes on every call to `$digest`.
   * - The `listener` is called whenever any expression in the `watchExpressions` array changes.
   *
   * @param {Array.any)>} watchExpressions Array of expressions that will be individually
   * watched using {@link ng.$rootScope.Scope#$watch $watch()}
   *
   * @param {function(any, any, Scope): any} listener Callback called whenever the return value of any
   *    expression in `watchExpressions` changes
   *    The `newValues` array contains the current values of the `watchExpressions`, with the indexes matching
   *    those of `watchExpression`
   *    and the `oldValues` array contains the previous values of the `watchExpressions`, with the indexes matching
   *    those of `watchExpression`
   *    The `scope` refers to the current scope.
   * @returns {function()} Returns a de-registration function for all listeners.
   */
  $watchGroup(watchExpressions, listener) {
    const oldValues = new Array(watchExpressions.length);
    const newValues = new Array(watchExpressions.length);
    const deregisterFns = [];
    const self = this;
    let changeReactionScheduled = false;
    let firstRun = true;

    if (!watchExpressions.length) {
      // No expressions means we call the listener ASAP
      let shouldCall = true;
      self.$evalAsync(() => {
        if (shouldCall) listener(newValues, newValues, self);
      });
      return function deregisterWatchGroup() {
        shouldCall = false;
      };
    }

    if (watchExpressions.length === 1) {
      // Special case size of one
      return this.$watch(watchExpressions[0], (value, oldValue, scope) => {
        newValues[0] = value;
        oldValues[0] = oldValue;
        listener(newValues, value === oldValue ? newValues : oldValues, scope);
      });
    }

    Object.entries(watchExpressions).forEach(([i, expr]) => {
      const unwatchFn = self.$watch(expr, (value) => {
        newValues[i] = value;
        if (!changeReactionScheduled) {
          changeReactionScheduled = true;
          self.$evalAsync(watchGroupAction);
        }
      });
      deregisterFns.push(unwatchFn);
    });

    function watchGroupAction() {
      changeReactionScheduled = false;

      try {
        if (firstRun) {
          firstRun = false;
          listener(newValues, newValues, self);
        } else {
          listener(newValues, oldValues, self);
        }
      } finally {
        for (let i = 0; i < watchExpressions.length; i++) {
          oldValues[i] = newValues[i];
        }
      }
    }

    return function deregisterWatchGroup() {
      while (deregisterFns.length) {
        deregisterFns.shift()();
      }
    };
  }

  /**
   * Shallow watches the properties of an object and fires whenever any of the properties change
   * (for arrays, this implies watching the array items; for object maps, this implies watching
   * the properties). If a change is detected, the `listener` callback is fired.
   *
   * - The `obj` collection is observed via standard $watch operation and is examined on every
   *   call to $digest() to see if any items have been added, removed, or moved.
   * - The `listener` is called whenever anything within the `obj` has changed. Examples include
   *   adding, removing, and moving items belonging to an object or array.
   *
   *
   *
   *
   * @param {string|function(Scope):any} obj Evaluated as {@link guide/expression expression}. The
   *    expression value should evaluate to an object or an array which is observed on each
   *    {@link ng.$rootScope.Scope#$digest $digest} cycle. Any shallow change within the
   *    collection will trigger a call to the `listener`.
   *
   * @param {function(any[], any[], Scope):any} listener a callback function called
   *    when a change is detected.
   *    - The `newCollection` object is the newly modified data obtained from the `obj` expression
   *    - The `oldCollection` object is a copy of the former collection data.
   *      Due to performance considerations, the`oldCollection` value is computed only if the
   *      `listener` function declares two or more arguments.
   *    - The `scope` argument refers to the current scope.
   *
   * @returns {function()} Returns a de-registration function for this listener. When the
   *    de-registration function is executed, the internal watch operation is terminated.
   */
  $watchCollection(obj, listener) {
    // Mark the interceptor as
    // ... $$pure when literal since the instance will change when any input changes
    $watchCollectionInterceptor.$$pure = $parse(obj).literal;
    // ... $stateful when non-literal since we must read the state of the collection
    $watchCollectionInterceptor.$stateful = !$watchCollectionInterceptor.$$pure;

    const self = this;
    // the current value, updated on each dirty-check run
    let newValue;
    // a shallow copy of the newValue from the last dirty-check run,
    // updated to match newValue during dirty-check run
    let oldValue;
    // a shallow copy of the newValue from when the last change happened
    let veryOldValue;
    // only track veryOldValue if the listener is asking for it
    const trackVeryOldValue = listener.length > 1;
    let changeDetected = 0;
    const changeDetector = $parse(obj, $watchCollectionInterceptor);
    const internalArray = [];
    let internalObject = {};
    let initRun = true;
    let oldLength = 0;

    function $watchCollectionInterceptor(_value) {
      newValue = _value;
      let newLength;
      let key;
      let bothNaN;
      let newItem;
      let oldItem;

      // If the new value is undefined, then return undefined as the watch may be a one-time watch
      if (isUndefined(newValue)) return;

      if (!isObject(newValue)) {
        // if primitive
        if (oldValue !== newValue) {
          oldValue = newValue;
          changeDetected++;
        }
      } else if (isArrayLike(newValue)) {
        if (oldValue !== internalArray) {
          // we are transitioning from something which was not an array into array.
          oldValue = internalArray;
          oldLength = oldValue.length = 0;
          changeDetected++;
        }

        newLength = newValue.length;

        if (oldLength !== newLength) {
          // if lengths do not match we need to trigger change notification
          changeDetected++;
          oldValue.length = oldLength = newLength;
        }
        // copy the items to oldValue and look for changes.
        for (let i = 0; i < newLength; i++) {
          oldItem = oldValue[i];
          newItem = newValue[i];

          bothNaN = oldItem !== oldItem && newItem !== newItem;
          if (!bothNaN && oldItem !== newItem) {
            changeDetected++;
            oldValue[i] = newItem;
          }
        }
      } else {
        if (oldValue !== internalObject) {
          // we are transitioning from something which was not an object into object.
          oldValue = internalObject = {};
          oldLength = 0;
          changeDetected++;
        }
        // copy the items to oldValue and look for changes.
        newLength = 0;
        for (key in newValue) {
          if (Object.hasOwnProperty.call(newValue, key)) {
            newLength++;
            newItem = newValue[key];
            oldItem = oldValue[key];

            if (key in oldValue) {
              bothNaN = oldItem !== oldItem && newItem !== newItem;
              if (!bothNaN && oldItem !== newItem) {
                changeDetected++;
                oldValue[key] = newItem;
              }
            } else {
              oldLength++;
              oldValue[key] = newItem;
              changeDetected++;
            }
          }
        }
        if (oldLength > newLength) {
          // we used to have more keys, need to find them and destroy them.
          changeDetected++;
          for (key in oldValue) {
            if (!Object.hasOwnProperty.call(newValue, key)) {
              oldLength--;
              delete oldValue[key];
            }
          }
        }
      }
      return changeDetected;
    }

    function $watchCollectionAction() {
      if (initRun) {
        initRun = false;
        listener(newValue, newValue, self);
      } else {
        listener(newValue, veryOldValue, self);
      }

      // make a copy for the next time a collection is changed
      if (trackVeryOldValue) {
        if (!isObject(newValue)) {
          // primitive
          veryOldValue = newValue;
        } else if (isArrayLike(newValue)) {
          veryOldValue = new Array(newValue.length);
          for (let i = 0; i < newValue.length; i++) {
            veryOldValue[i] = newValue[i];
          }
        } else {
          // if object
          veryOldValue = {};
          for (const key in newValue) {
            if (Object.hasOwnProperty.call(newValue, key)) {
              veryOldValue[key] = newValue[key];
            }
          }
        }
      }
    }
    return this.$watch(changeDetector, $watchCollectionAction);
  }

  /**
 * Processes all of the {@link ng.$rootScope.Scope#$watch watchers} of the current scope and
 * its children. Because a {@link ng.$rootScope.Scope#$watch watcher}'s listener can change
 * the model, the `$digest()` keeps calling the {@link ng.$rootScope.Scope#$watch watchers}
 * until no more listeners are firing. This means that it is possible to get into an infinite
 * loop. This function will throw `'Maximum iteration limit exceeded.'` if the number of
 * iterations exceeds 10.
 *
 * Usually, you don't call `$digest()` directly in
 * {@link ng.directive:ngController controllers} or in
 * {@link ng.$compileProvider#directive directives}.
 * Instead, you should call {@link ng.$rootScope.Scope#$apply $apply()} (typically from within
 * a {@link ng.$compileProvider#directive directive}), which will force a `$digest()`.
 *
 * If you want to be notified whenever `$digest()` is called,
 * you can register a `watchExpression` function with
 * {@link ng.$rootScope.Scope#$watch $watch()} with no `listener`.
 *
 * In unit tests, you may need to call `$digest()` to simulate the scope life cycle.
 *
 * @example
 * ```js
     let scope = ...;
     scope.name = 'misko';
     scope.counter = 0;

     expect(scope.counter).toEqual(0);
     scope.$watch('name', function(newValue, oldValue) {
       scope.counter = scope.counter + 1;
     });
     expect(scope.counter).toEqual(0);

     ;
     // the listener is always called during the first $digest loop after it was registered
     expect(scope.counter).toEqual(1);

     ;
     // but now it will not be called unless the value changes
     expect(scope.counter).toEqual(1);

     scope.name = 'adam';
     ;
     expect(scope.counter).toEqual(2);
 * ```
 *
 */
  $digest() {
    let value;
    let last;
    let fn;
    let get;
    let watchers;
    let dirty;
    let ttl = TTL;
    let next;
    /** @type {Scope} */
    let current;
    /** @type {Scope} */
    const target = $$asyncQueue.length ? this.$root : this;
    const watchLog = [];
    let logIdx;
    let asyncTask;

    this.beginPhase(ScopePhase.DIGEST);
    // Check for changes to browser url that happened in sync before the call to $digest
    // TODO Implement browser
    $browser.$$checkUrlChange();

    if (this.isRoot && applyAsyncId !== null) {
      // If this is the root scope, and $applyAsync has scheduled a deferred $apply(), then
      // cancel the scheduled $apply and flush the queue of expressions to be evaluated.
      $browser.cancel(applyAsyncId);
      flushApplyAsync();
      applyAsyncId = null;
    }

    lastDirtyWatch = null;
    do {
      // "while dirty" loop
      dirty = false;
      current = target;

      // It's safe for asyncQueuePosition to be a local variable here because this loop can't
      // be reentered recursively. Calling $digest from a function passed to $evalAsync would
      // lead to a '$digest already in progress' error.
      for (
        let asyncQueuePosition = 0;
        asyncQueuePosition < $$asyncQueue.length;
        asyncQueuePosition++
      ) {
        try {
          asyncTask = $$asyncQueue[asyncQueuePosition];
          fn = asyncTask.fn;
          fn(asyncTask.scope, asyncTask.locals);
        } catch (e) {
          $exceptionHandler(e);
        }
        lastDirtyWatch = null;
      }
      $$asyncQueue.length = 0;

      do {
        // "traverse the scopes" loop
        if ((watchers = !current.$$suspended && current.$$watchers)) {
          // process our watches
          current.$$digestWatchIndex = watchers.length;
          while (current.$$digestWatchIndex--) {
            try {
              const watch = watchers[current.$$digestWatchIndex];
              // Most common watches are on primitives, in which case we can short
              // circuit it with === operator, only when === fails do we use .equals
              if (watch) {
                get = watch.get;
                if (
                  (value = get(current)) !== (last = watch.last) &&
                  !(watch.eq
                    ? equals(value, last)
                    : isNumberNaN(value) && isNumberNaN(last))
                ) {
                  dirty = true;
                  lastDirtyWatch = watch;
                  watch.last = watch.eq ? structuredClone(value) : value;
                  fn = watch.fn;
                  fn(value, last === initWatchVal ? value : last, current);
                  if (ttl < 5) {
                    logIdx = 4 - ttl;
                    if (!watchLog[logIdx]) watchLog[logIdx] = [];
                    watchLog[logIdx].push({
                      msg: isFunction(watch.exp)
                        ? `fn: ${watch.exp.name || watch.exp.toString()}`
                        : watch.exp,
                      newVal: value,
                      oldVal: last,
                    });
                  }
                } else if (watch === lastDirtyWatch) {
                  // If the most recently dirty watcher is now clean, short circuit since the remaining watchers
                  // have already been tested.
                  dirty = false;
                  break;
                }
              }
            } catch (e) {
              $exceptionHandler(e);
            }
          }
        }

        // Insanity Warning: scope depth-first traversal
        // yes, this code is a bit crazy, but it works and we have tests to prove it!
        // this piece should be kept in sync with the traversal in $broadcast
        // (though it differs due to having the extra check for $$suspended and does not
        // check $$listenerCount)
        if (
          !(next =
            (!current.$$suspended &&
              current.$$watchersCount &&
              current.$$childHead) ||
            (current !== target && current.$$nextSibling))
        ) {
          while (current !== target && !(next = current.$$nextSibling)) {
            current = current.$parent;
          }
        }
      } while ((current = next));

      // `break traverseScopesLoop;` takes us to here

      if ((dirty || $$asyncQueue.length) && !ttl--) {
        this.clearPhase();
        throw $rootScopeMinErr(
          "infdig",
          "{0} $digest() iterations reached. Aborting!\n" +
            "Watchers fired in the last 5 iterations: {1}",
          TTL,
          watchLog,
        );
      }
    } while (dirty || $$asyncQueue.length);

    this.clearPhase();

    // postDigestQueuePosition isn't local here because this loop can be reentered recursively.
    while (postDigestQueuePosition < $postUpdateQueue.length) {
      try {
        $postUpdateQueue[postDigestQueuePosition++]();
      } catch (e) {
        $exceptionHandler(e);
      }
    }
    $postUpdateQueue.length = postDigestQueuePosition = 0;

    // Check for changes to browser url that happened during the $digest
    // (for which no event is fired; e.g. via `history.pushState()`)
    $browser.$$checkUrlChange();
  }

  /**
   * @param {ScopePhase} phase
   */
  beginPhase(phase) {
    if (this.$root.$$phase !== ScopePhase.NONE) {
      throw $rootScopeMinErr(
        "inprog",
        "digest already in progress",
        this.$root.$$phase,
      );
    }

    this.$root.$$phase = phase;
  }

  /**
   * Suspend watchers of this scope subtree so that they will not be invoked during digest.
   *
   * This can be used to optimize your application when you know that running those watchers
   * is redundant.
   *
   * **Warning**
   *
   * Suspending scopes from the digest cycle can have unwanted and difficult to debug results.
   * Only use this approach if you are confident that you know what you are doing and have
   * ample tests to ensure that bindings get updated as you expect.
   *
   * Some of the things to consider are:
   *
   * * Any external event on a directive/component will not trigger a digest while the hosting
   *   scope is suspended - even if the event handler calls `$apply()` or ``.
   * * Transcluded content exists on a scope that inherits from outside a directive but exists
   *   as a child of the directive's containing scope. If the containing scope is suspended the
   *   transcluded scope will also be suspended, even if the scope from which the transcluded
   *   scope inherits is not suspended.
   * * Multiple directives trying to manage the suspended status of a scope can confuse each other:
   *    * A call to `$suspend()` on an already suspended scope is a no-op.
   *    * A call to `$resume()` on a non-suspended scope is a no-op.
   *    * If two directives suspend a scope, then one of them resumes the scope, the scope will no
   *      longer be suspended. This could result in the other directive believing a scope to be
   *      suspended when it is not.
   * * If a parent scope is suspended then all its descendants will be also excluded from future
   *   digests whether or not they have been suspended themselves. Note that this also applies to
   *   isolate child scopes.
   * * Calling `$digest()` directly on a descendant of a suspended scope will still run the watchers
   *   for that scope and its descendants. When digesting we only check whether the current scope is
   *   locally suspended, rather than checking whether it has a suspended ancestor.
   * * Calling `$resume()` on a scope that has a suspended ancestor will not cause the scope to be
   *   included in future digests until all its ancestors have been resumed.
   * * Resolved promises, e.g. from explicit `$q` deferreds and `$http` calls, trigger `$apply()`
   *   against the `$rootScope` and so will still trigger a global digest even if the promise was
   *   initiated by a component that lives on a suspended scope.
   */
  $suspend() {
    this.$$suspended = true;
  }

  /**

   * Call this method to determine if this scope has been explicitly suspended. It will not
   * tell you whether an ancestor has been suspended.
   * To determine if this scope will be excluded from a digest triggered at the $rootScope,
   * for example, you must check all its ancestors:
   *
   * ```
   * function isExcludedFromDigest(scope) {
   *   while(scope) {
   *     if (scope.$isSuspended()) return true;
   *     scope = scope.$parent;
   *   }
   *   return false;
   * ```
   *
   * Be aware that a scope may not be included in digests if it has a suspended ancestor,
   * even if `$isSuspended()` returns false.
   *
   * @returns true if the current scope has been suspended.
   */
  $isSuspended() {
    return this.$$suspended;
  }

  /**
   * Resume watchers of this scope subtree in case it was suspended.
   *
   * See {@link $rootScope.Scope#$suspend} for information about the dangers of using this approach.
   */
  $resume() {
    this.$$suspended = false;
  }

  /**
   * Broadcasted when a scope and its children are being destroyed.
   *
   * Note that, in AngularTS, there is also a `$destroy` jQuery event, which can be used to
   * clean up DOM bindings before an element is removed from the DOM.
   */

  /**
   * Removes the current scope (and all of its children) from the parent scope. Removal implies
   * that calls to {@link ng.$rootScope.Scope#$digest $digest()} will no longer
   * propagate to the current scope and its children. Removal also implies that the current
   * scope is eligible for garbage collection.
   *
   * The `$destroy()` is usually used by directives such as
   * {@link ng.directive:ngRepeat ngRepeat} for managing the
   * unrolling of the loop.
   *
   * Just before a scope is destroyed, a `$destroy` event is broadcasted on this scope.
   * Application code can register a `$destroy` event handler that will give it a chance to
   * perform any necessary cleanup.
   *
   * Note that, in AngularTS, there is also a `$destroy` event, which can be used to
   * clean up DOM bindings before an element is removed from the DOM.
   */
  $destroy() {
    // We can't destroy a scope that has been already destroyed.
    if (this.$$destroyed) return;

    this.$broadcast("$destroy");
    this.$$destroyed = true;

    if (this === this.$root) {
      // Remove handlers attached to window when $rootScope is removed
      $browser.$$applicationDestroyed();
    }

    this.incrementWatchersCount(-this.$$watchersCount);
    for (const eventName in this.$$listenerCount) {
      this.decrementListenerCount(this.$$listenerCount[eventName], eventName);
    }

    // sever all the references to parent scopes (after this cleanup, the current scope should
    // not be retained by any of our references and should be eligible for garbage collection)
    if (this.$parent) {
      if (this.$parent.$$childHead === this)
        this.$parent.$$childHead = this.$$nextSibling;
      if (this.$parent.$$childTail === this)
        this.$parent.$$childTail = this.$$prevSibling;
    }
    if (this.$$prevSibling)
      this.$$prevSibling.$$nextSibling = this.$$nextSibling;
    if (this.$$nextSibling)
      this.$$nextSibling.$$prevSibling = this.$$prevSibling;

    // Disable listeners, watchers and apply/digest methods
    this.$destroy = this.$digest = this.$apply = this.$applyAsync = () => {};
    this.$evalAsync = () => undefined;
    this.$on =
      this.$watch =
      this.$watchGroup =
        function () {
          return () => {};
        };
    this.$$listeners.clear();

    // Disconnect the next sibling to prevent `cleanUpScope` destroying those too
    this.$$nextSibling = null;
    this.$parent = null;
    this.$$nextSibling = null;
    this.$$prevSibling = null;
    this.$$childHead = null;
    this.$$childTail = null;
    this.$root = null;
    this.$$watchers = null;
  }

  /**
 * Executes the `expression` on the current scope and returns the result. Any exceptions in
 * the expression are propagated (uncaught). This is useful when evaluating AngularTS
 * expressions.
 *
 * @example
 * ```js
     let scope = new Scope();
     scope.a = 1;
     scope.b = 2;

     expect(scope.$eval('a+b')).toEqual(3);
     expect(scope.$eval(function(scope){ return scope.a + scope.b; })).toEqual(3);
 * ```
 *
 * @param {string|function(Scope): any} [expr] An AngularTS expression to be executed.
 *
 *    - `string`: execute using the rules as defined in  {@link guide/expression expression}.
 *    - `function(scope)`: execute the function with the current `scope` parameter.
 *
 * @param {(object)=} locals Local variables object, useful for overriding values in scope.
 * @returns {*} The result of evaluating the expression.
 */
  $eval(expr, locals) {
    return $parse(expr)(this, locals);
  }

  /**
   * Executes the expression on the current scope at a later point in time.
   *
   * The `$evalAsync` makes no guarantees as to when the `expression` will be executed, only
   * that:
   *
   *   - it will execute after the function that scheduled the evaluation (preferably before DOM
   *     rendering).
   *   - at least one {@link ng.$rootScope.Scope#$digest $digest cycle} will be performed after
   *     `expression` execution.
   *
   * Any exceptions from the execution of the expression are forwarded to the
   * {@link ng.$exceptionHandler $exceptionHandler} service.
   *
   * __Note:__ if this function is called outside of a `$digest` cycle, a new `$digest` cycle
   * will be scheduled. However, it is encouraged to always call code that changes the model
   * from within an `$apply` call. That includes code evaluated via `$evalAsync`.
   *
   * @param {(string|function(any):any)=} expr An AngularTS expression to be executed.
   *
   *    - `string`: execute using the rules as defined in {@link guide/expression expression}.
   *    - `function(scope)`: execute the function with the current `scope` parameter.
   *
   * @param {(object)=} locals Local variables object, useful for overriding values in scope.
   */
  $evalAsync(expr, locals) {
    // if we are outside of an $digest loop and this is the first time we are scheduling async
    // task also schedule async auto-flush
    let id;
    if (this.$root.$$phase === ScopePhase.NONE && !$$asyncQueue.length) {
      id = $browser.defer(
        () => {
          if ($$asyncQueue.length) {
            this.$root.$digest();
          }
        },
        null,
        "$evalAsync",
      );
    }

    $$asyncQueue.push({
      scope: this,
      fn: $parse(expr),
      locals,
    });

    return id;
  }

  $postUpdate(fn) {
    $postUpdateQueue.push(fn);
  }

  /**
 * `$apply()` is used to execute an expression in AngularTS from outside of the AngularTS
 * framework. (For example from browser DOM events, setTimeout, XHR or third party libraries).
 * Because we are calling into the AngularTS framework we need to perform proper scope life
 * cycle of {@link ng.$exceptionHandler exception handling},
 * {@link ng.$rootScope.Scope#$digest executing watches}.
 *
 * **Life cycle: Pseudo-Code of `$apply()`**
 *
 * ```js
     function $apply(expr) {
       try {
         return $eval(expr);
       } catch (e) {
         $exceptionHandler(e);
       } finally {
         $root.$digest();
       }
     }
 * ```
 *
 *
 * Scope's `$apply()` method transitions through the following stages:
 *
 * 1. The {@link guide/expression expression} is executed using the
 *    {@link ng.$rootScope.Scope#$eval $eval()} method.
 * 2. Any exceptions from the execution of the expression are forwarded to the
 *    {@link ng.$exceptionHandler $exceptionHandler} service.
 * 3. The {@link ng.$rootScope.Scope#$watch watch} listeners are fired immediately after the
 *    expression was executed using the {@link ng.$rootScope.Scope#$digest $digest()} method.
 *
 *
 * @param {string|function(Scope): any} [expr] An AngularTS expression to be executed.
 *
 *    - `string`: execute using the rules as defined in {@link guide/expression expression}.
 *    - `function(scope)`: execute the function with current `scope` parameter.
 *
 * @returns {*} The result of evaluating the expression.
 */
  $apply(expr) {
    try {
      this.beginPhase(ScopePhase.APPLY);
      try {
        return this.$eval(expr);
      } finally {
        this.clearPhase();
      }
    } catch (e) {
      $exceptionHandler(e);
    } finally {
      this.retry();
    }
  }

  /**
   * @private
   */
  retry() {
    try {
      this.$root.$digest();
    } catch (e) {
      $exceptionHandler(e);
      throw e;
    }
  }

  clearPhase() {
    this.$root.$$phase = ScopePhase.NONE;
  }

  /**
   * Schedule the invocation of $apply to occur at a later time. The actual time difference
   * varies across browsers, but is typically around ~10 milliseconds.
   *
   * This can be used to queue up multiple expressions which need to be evaluated in the same
   * digest.
   *
   * @param {(string|function())=} expr An AngularTS expression to be executed.
   *
   *    - `string`: execute using the rules as defined in {@link guide/expression expression}.
   *    - `function(scope)`: execute the function with current `scope` parameter.
   */
  $applyAsync(expr) {
    const scope = this;
    if (expr) {
      $$applyAsyncQueue.push(() => scope.$eval(expr));
    }
    // TODO: investigate
    //expr = $parse(expr);

    if (applyAsyncId === null) {
      applyAsyncId = $browser.defer(flushApplyAsync, null, "$applyAsync");
    }
  }

  /**
   * @description
   * Listens on events of a given type. See {@link ng.$rootScope.Scope#$emit $emit} for
   * discussion of event life cycle.
   *
   * The event listener function format is: `function(event, args...)`. The `event` object
   * passed into the listener has the following attributes:
   *
   *   - `targetScope` - `{Scope}`: the scope on which the event was `$emit`-ed or
   *     `$broadcast`-ed.
   *   - `currentScope` - `{Scope}`: the scope that is currently handling the event. Once the
   *     event propagates through the scope hierarchy, this property is set to null.
   *   - `name` - `{string}`: name of the event.
   *   - `stopPropagation` - `{function=}`: calling `stopPropagation` function will cancel
   *     further event propagation (available only for events that were `$emit`-ed).
   *   - `preventDefault` - `{function}`: calling `preventDefault` sets `defaultPrevented` flag
   *     to true.
   *   - `defaultPrevented` - `{boolean}`: true if `preventDefault` was called.
   *
   * @param {string} name Event name to listen on.
   * @param {function(any): any} listener Function to call when the event is emitted witn angular.IAngularEvent
   * @returns {function()} Returns a deregistration function for this listener.
   */
  $on(name, listener) {
    let namedListeners = this.$$listeners.get(name);
    if (!namedListeners) {
      namedListeners = [];
      this.$$listeners.set(name, namedListeners);
    }
    namedListeners.push(listener);

    /** @type {Scope} */
    let current = this;
    do {
      current.$$listenerCount[name] = (current.$$listenerCount[name] ?? 0) + 1;
    } while ((current = current.$parent));

    return () => {
      const indexOfListener = namedListeners.indexOf(listener);
      if (indexOfListener !== -1) {
        // Use delete in the hope of the browser deallocating the memory for the array entry,
        // while not shifting the array indexes of other listeners.
        // See issue https://github.com/angular/angular.js/issues/16135
        delete namedListeners[indexOfListener];
        this.decrementListenerCount(1, name);
      }
    };
  }

  /**
   * @param {number} count
   */
  incrementWatchersCount(count) {
    this.$$watchersCount += count;
    if (this.$parent) {
      this.$parent.incrementWatchersCount(count);
    }
  }

  /**
   * @param {number} count
   * @param {string} name
   */
  decrementListenerCount(count, name) {
    /** @type {Scope} */
    let self = this;
    for (; self; self = self.$parent) {
      if (self.$$listenerCount[name] !== undefined) {
        self.$$listenerCount[name] -= count;

        if (self.$$listenerCount[name] === 0) {
          delete self.$$listenerCount[name];
        }
      }
    }
  }

  /**
   * Dispatches an event `name` upwards through the scope hierarchy notifying the
   * registered {@link ng.$rootScope.Scope#$on} listeners.
   *
   * The event life cycle starts at the scope on which `$emit` was called. All
   * {@link ng.$rootScope.Scope#$on listeners} listening for `name` event on this scope get
   * notified. Afterwards, the event traverses upwards toward the root scope and calls all
   * registered listeners along the way. The event will stop propagating if one of the listeners
   * cancels it.
   *
   * Any exception emitted from the {@link ng.$rootScope.Scope#$on listeners} will be passed
   * onto the {@link ng.$exceptionHandler $exceptionHandler} service.
   *
   * @param {string} name Event name to emit.
   * @param {...*} args Optional one or more arguments which will be passed onto the event listeners.
   * @return {Object} Event object (see {@link ng.$rootScope.Scope#$on}).
   */
  $emit(name, ...args) {
    const empty = [];
    let namedListeners;
    /** @type {Scope} */
    let scope = this;
    let stopPropagation = false;
    const event = {
      name,
      targetScope: scope,
      stopPropagation() {
        stopPropagation = true;
      },
      preventDefault() {
        event.defaultPrevented = true;
      },
      defaultPrevented: false,
    };
    const listenerArgs = concat([event], [event].concat(args), 1);
    let i;
    let length;

    do {
      namedListeners = scope.$$listeners.get(name) || empty;
      event.currentScope = scope;
      for (i = 0, length = namedListeners.length; i < length; i++) {
        // if listeners were deregistered, defragment the array
        if (!namedListeners[i]) {
          namedListeners.splice(i, 1);
          i--;
          length--;
          continue;
        }
        try {
          // allow all listeners attached to the current scope to run
          namedListeners[i].apply(null, listenerArgs);
        } catch (e) {
          $exceptionHandler(e);
        }
      }
      // if any listener on the current scope stops propagation, prevent bubbling
      if (stopPropagation) {
        break;
      }
      // traverse upwards
      scope = /** @type {Scope} */ scope.$parent;
    } while (scope);

    event.currentScope = null;

    return event;
  }

  /**
   * Dispatches an event `name` downwards to all child scopes (and their children) notifying the
   * registered {@link ng.$rootScope.Scope#$on} listeners.
   *
   * The event life cycle starts at the scope on which `$broadcast` was called. All
   * {@link ng.$rootScope.Scope#$on listeners} listening for `name` event on this scope get
   * notified. Afterwards, the event propagates to all direct and indirect scopes of the current
   * scope and calls all registered listeners along the way. The event cannot be canceled.
   *
   * Any exception emitted from the {@link ng.$rootScope.Scope#$on listeners} will be passed
   * onto the {@link ng.$exceptionHandler $exceptionHandler} service.
   *
   * @param {string} name Event name to broadcast.
   * @param {...*} args Optional one or more arguments which will be passed onto the event listeners.
   * @return {Object} Event object, see {@link ng.$rootScope.Scope#$on}
   */
  $broadcast(name, ...args) {
    const target = this;
    /** @type {Scope} */
    let current = target;

    /** @type {Scope} */
    let next = target;
    const event = {
      name,
      targetScope: target,
      preventDefault() {
        event.defaultPrevented = true;
      },
      defaultPrevented: false,
    };

    if (!target.$$listenerCount[name]) return event;

    const listenerArgs = concat([event], [event].concat(args), 1);
    let listeners;
    let i;
    let length;

    // down while you can, then up and next sibling or up and next sibling until back at root
    while ((current = next)) {
      event.currentScope = current;
      listeners = current.$$listeners.get(name) || [];
      for (i = 0, length = listeners.length; i < length; i++) {
        // if listeners were deregistered, defragment the array
        if (!listeners[i]) {
          listeners.splice(i, 1);
          i--;
          length--;
          continue;
        }

        try {
          listeners[i].apply(null, listenerArgs);
        } catch (e) {
          $exceptionHandler(e);
        }
      }

      // Insanity Warning: scope depth-first traversal
      // yes, this code is a bit crazy, but it works and we have tests to prove it!
      // this piece should be kept in sync with the traversal in $digest
      // (though it differs due to having the extra check for $$listenerCount and
      // does not check $$suspended)
      if (
        !(next =
          (current.$$listenerCount[name] && current.$$childHead) ||
          (current !== target && current.$$nextSibling))
      ) {
        // TODO: current check fixes "contents are destroyed along with transcluding directive" test which sets current to null
        while (
          current &&
          current !== target &&
          !(next = current.$$nextSibling)
        ) {
          current = current.$parent;
        }
      }
    }

    event.currentScope = null;
    return event;
  }
}

/**
 * function used as an initial value for watchers.
 * because it's unique we can easily tell it apart from other values
 */
function initWatchVal() {}

function flushApplyAsync() {
  while ($$applyAsyncQueue.length) {
    try {
      $$applyAsyncQueue.shift()();
    } catch (e) {
      $exceptionHandler(e);
    }
  }
  applyAsyncId = null;
}

/**
 * Counts all the watchers of direct and indirect child scopes of the current scope.
 *
 * The watchers of the current scope are included in the count and so are all the watchers of
 * isolate child scopes.
 * @param {Scope} scope
 * @returns {number} Total number of watchers.
 */
export function countWatchers(scope) {
  var count = scope.$$watchers ? scope.$$watchers.length : 0; // include the current scope
  var pendingChildHeads = [scope.$$childHead];
  var currentScope;

  while (pendingChildHeads.length) {
    currentScope = pendingChildHeads.shift();

    while (currentScope) {
      count += currentScope.$$watchers ? currentScope.$$watchers.length : 0;
      pendingChildHeads.push(currentScope.$$childHead);
      currentScope = currentScope.$$nextSibling;
    }
  }

  return count;
}

/**
 * Counts all the direct and indirect child scopes of the current scope.
 *
 * The current scope is excluded from the count. The count includes all isolate child scopes.
 * @param {Scope} scope
 * @returns {number} Total number of child scopes.
 */
export function countChildScopes(scope) {
  var count = 0; // exclude the current scope
  var pendingChildHeads = [scope.$$childHead];
  var currentScope;

  while (pendingChildHeads.length) {
    currentScope = pendingChildHeads.shift();

    while (currentScope) {
      count += 1;
      pendingChildHeads.push(currentScope.$$childHead);
      currentScope = currentScope.$$nextSibling;
    }
  }

  return count;
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy