11.1 $watch

El método $watch nos permite saber cuando se ha cambiado algún dato de alguna de las propiedades del $scope. Es decir es como un onChange pero en vez de sobre un tag <input> o similar es sobre una variable JavaScript. Esta que puede parecer un pequeño cambio supone una gran ventaja en el desarrollo de aplicaciones JavaScript.

¿Porqué es un gran cambio? Uno de los patrones mas famosos es el MVC. Lo mas importante de este patrón es separar la vista del controlador y la vista del modelo, ya que entraría dentro de los normal modificar la vista sin que afectara ni al modelo ni al controlador. Pues bien , justamente el método $watch nos ayuda en dicha separación.

Pasemos ahora a ver la forma as sencilla de como funciona $watch. Este método acepta 2 parámetros:

  • watchExpression : El nombre de la propiedaddel $scope que queremos monitorizar para que nos avisen cuando cambie su valor.
  • listener: Es la función de callback que será llamada cuando cambie el valor de la propiedad especificada en watchExpression. Esta función acepta 2 parámetros, el primero con el nuevo valor y segundo con el valor antiguo. Hay que tener en cuenta que esta función siempre se llama una primera vez con el valor que tiene tras ejecutarse el código del controlador. Pero en ese caso los 2 parámetros tendrán el mismo valor.

app.controller("PruebaController", function($scope) {
  
  $scope.nombre="Lorenzo"
  
  $scope.$watch("nombre",function(newValue,oldValue) {
    
    if (newValue===oldValue) {
      return;
    }
    
   alert("El nuevo valor es " + newValue);
  });
  
  $scope.change=function() {
    $scope.nombre="Juan"
  }
  
  
});

  • Línea 5: Definimos que queremos monitorizar el valor de la propiedad nombre, y definimos la función de callback con los 2 parámetros del nuevo valor y del viejo valor.
  • Línea 7: Siempre debemos comprobar si el nuevo valor es igual al antiguo ya que la primera vez que se llama son iguales.
  • Línea 8: Si son iguales en este caso no queremos hacer nada y acabamos con un return.
  • Línea 11: Ya hemos comprobado que los valores son distintos y ahora que cuando hacemos lo que necesitemos hacer. En nuestro ejemplo es simplemente mostrar un alert.
  • Líneas 14 y 15: Si llamáramos a la función change desde un ng-click o desde donde queramos , como cambia el valor de la propiedad nombre se ejecutará la función de callback del $watch.

Recuerda que es una buena idea comprobar si el valor nuevo es distinto al valor viejo.
if (newValue===oldValue) {
}

La importancia de $watch

Como vemos es muy sencillo el uso de $watch. Pero , ¿porque es tan importante $watch? Hasta ahora hemos podido hacer cosas similares usando los eventos onClick o onChange ¿Que tiene de diferente? Pues la diferencia es que ya no estamos monitorizando cambios en la vista por ejemplo mediante onChange sino que directamente monitorizamos el propio modelo, eso hace que separemos aun mas la vista del modelo, lo que ayuda a seguir el patrón MVC.

Si lo que realmente queremos es saber cuando ha cambiado el valor de una propiedad de modelo, ¿Porque usar el evento onChange para comprobar sise ha modificado? ¿Que ocurre si modificamos la vista? Hay que recordar poner siempre ese onChange y por otro lado y si cambiamos directamente desde JavaScript el valor haciendo una asignación a la propiedad. Con onChange no lo podríamos detectar. Es decir que $watch es mucho mas útil que los eventos del DOM ya que nos permite detectar siempre el cambio independientemente desde donde se haya producido.

Siempre que puedas utiliza $watch en vez de eventos de los tag HTML como onChange , etc, ya que así estará mas separado el modelo de la vista.

Comparando el objeto

¿Como sabe AngularJS que ha cambiado el valor de una propiedad? La respuesta puede parecer sencilla, con un simple ”!==”. Eso funciona muy bien con datos escalares como números o texto pero no funciona tan bien con objetos. Si comparamos 2 objetos distintos pero con los mismos valores,¿deberían ser iguales o distintos? Angular al usar ”!==” serian distintos.

app.controller("PruebaController", function($scope) {

  $scope.persona = {
    nombre: "Lorenzo",
    ape1: "Garcia"
  }

  $scope.$watch("persona", function(newValue, oldValue) {
    if (newValue === oldValue) {
      return;
    }

    alert("El valor ha cambiado");
  });

  $scope.change = function() {
    $scope.persona = {
      nombre: "Lorenzo",
      ape1: "Garcia"
    }
  }


});

Como todos sabemos los objetos definidos en las líneas 3 y 17 son distintos por lo que se ejecutará la función de callback del $watch.

¿Que ocurre si queremos que angular trate los 2 objetos como iguales? Es muy sencillo, solo tenemos que pasarle un tercer argumento a la función de $watch con el valor true para que en vez de usar ”!==” use la función angular.equals que si que tiene en cuenta los valores de las propiedades.1).

Si modificamos el anterior código añadiendo el true ya no se ejecutará la función de callback del $watch

app.controller("PruebaController", function($scope) {

  $scope.persona = {
    nombre: "Lorenzo",
    ape1: "Garcia"
  }

  $scope.$watch("persona", function(newValue, oldValue) {
    if (newValue === oldValue) {
      return;
    }

    alert("El valor ha cambiado");
  },true);

  $scope.change = function() {
    $scope.persona = {
      nombre: "Lorenzo",
      ape1: "Garcia"
    }
  }


});

  • Línea 14: Al incluir como tercer parámetro del $watch el valor true ya no se comparan los valores mediante ”!==” sino mediante angular.equals.

Expresión a monitorizar

Hemos indicado que el primer parámetro de $watch es el valor de una propiedad , esto no es del todo cierto ya que que podemos indicar cualquier expresión de AngularJS sabiendo que estará referida al $scope. Eso incluye operaciones con propiedades o llamadas a funciones. La expresión que podemos en el primer parámetro es la misma que podríamos poner entre las llaves {{ }} en una página HTML. Angular te monitorizará el valor que pongas en la expresión.

app.controller("PruebaController", function($scope) {

  $scope.persona = {
    nombre: "Lorenzo",
    ape1: "Garcia"
  }

  $scope.$watch("getNombreCompleto()", function(newValue, oldValue) {
    if (newValue === oldValue) {
      return;
    }

    alert("El valor ha cambiado");
  });

  $scope.getNombreCompleto=function() {
    return $scope.persona.nombre + " " + $scope.persona.ape1
  }

  $scope.change = function() {
    $scope.persona.nombre="Juan";
  }


});

  • Línea 8: Ahora en vez de monitorizar el valor de una propiedad estamos monitorizando el valor de la función getNombreCompleto(). Obviamente debe ser una función definida en el $scope. Ahora en newValue tendremos el resultado de evaluar la función getNombreCompleto()
  • Línea 16: Es la función que estamos monitorizando para ver si cambia su valor.
  • Línea 21: El cambio que produce la línea 21 ahora es una propiedad del $scope pero eso genera un cambio en el valor que retorna getNombreCompleto() por lo que se dispara la función del callback del $watch.

Debido a que la expresión puede ser evaluada muchas veces, dicha expresión debería ser idempotente, es decir que no tenga efectos laterales. También para mejorar el rendimiento la función debería poder evaluarse rápidamente.

Debido a que la expresión puede ser evaluada muchas veces, dicha expresión debería de costar poco tiempo en ser evaluada.

$watchCollection

Otra opción que permite AngularJS en monitorizar si cambia algo de un array. En principio se podría hacer con el tercer parámetro de $watch con el valor de true pero esta opción puede ser demasiado costosa ya que hay que comprobar todas y cada una de las propiedades de cada uno de los objetos del array.

AngularJS dispone del método $watchCollection el cual comprobará para una expresión cuyo resultado es un array si se a añadido o borrado un elemento del array o si alguno de los elementos del array apunta a un nuevo objeto.

app.controller("PruebaController", function($scope) {

  $scope.provincias = [{
    idProvincia: 1,
    nombre: "Valencia"
  }, {
    idProvincia: 2,
    nombre: "Castellón"
  }, {
    idProvincia: 3,
    nombre: "Alicante"
  }];

  $scope.$watchCollection('provincias', function(newCollection, oldCollection) {

    if (newCollection === oldCollection) {
      return;
    }

    alert("El nuevo valor es:" + newCollection);
  });



  $scope.change1 = function() {
    $scope.provincias.pop();
  };

  $scope.change2 = function() {
    $scope.provincias[0] = {
      idProvincia: 1,
      nombre: "Valencia"
    }
  };

});

  • Línea 3: El modelo ahora a monitorizar es un array provincias.
  • Línea 14: Ahora usamos la función $watchCollection. Fijate que ahora la función de callback ahora tiene como parámetros los 2 array y no valores escalares.
  • Línea 26: Si llamamos al método pop() quitando el último elemento , se ejecutará la función de callback de $watchCollection
  • Línea 30: Si cambiamos el valor de un elemento del array tambien se ejecutará la función de callback de $watchCollection. Lo que hay que tener en ese caso es que los valores de las propiedades del nuevo objeto son iguales al que ya hay pero en ese caso $watchCollection como es un nuevo objeto si que se detecta el cambio.

$watchGroup

AngularJS 1.3 dispone de un nuevo método para comprobar mas de un atributo a la vez llamado $watchGroup a éste método se le pasa en vez de una expresión un array de expresiones. La función de callback será llamada si al menos una de las expresiones ha cambiado su valor. Este método no es mas que una pequeña ayuda para cuando hay varias propiedades que si cambian queremos tratarlas todas de la misma manera.

Su funcionamiento es el siguiente:

app.controller("PruebaController", function($scope) {
 
  $scope.persona = {
    nombre: "Lorenzo",
    ape1: "Garcia"
  }
 
  $scope.$watchGroup(['persona.nombre','persona.ape1'], function(newValues, oldValues) {
    if (newValues === oldValues) {
      return;
    }
 
    alert("Nuevo nombre:" + newValues[0]  + " y nuevo apellido:" + newValues[1]);
  });
 
  $scope.change1 = function() {
    $scope.persona.nombre="Juan";
  }
  
  $scope.change2 = function() {
    $scope.persona.ape1="Perez";
  }  
  
});

  • Línea 8: Usamos el método $watchGroup para monitorizar 2 propiedades: persona.nombre y persona.ape1.
  • Línea 13: Ahora los valores son un array con cada uno de los valores de cada expresión, en el mismo orden en el que se definieron.
  • Línea 16 y 20: Tanto si se cambia una propiedad u otra se lanzará la función de callback de $watchGroup.

Recuerda de $watchGroup solo funcionará con Angular 1.3.0

Rendimiento

Ahora una sería de consejos sobre rendimiento de $watch o cualquiera de sus dos variantes.

  • Incluir muchos $watchtiene un coste de rendimiento de la aplicación. Poner unos cuantos no va a suponer ningún problema pero en caso de tener que monitorizar muchos datos habría que tenerlo en cuenta.
  • Usar $watchGroup en vez de distintos $watch no supone ninguna mejora en el rendimiento ya que internamente se llamará para cada expresión a $watch.
  • En $watch poner a true el tercer parámetro también supone un coste ya que hay que comparar propiedad por propiedad del objeto.

Antipatrón

En algún blog de internet como en Random tricks when using AngularJS o en 5 Quick Angular JS Tips and Tricks se indica que para mejorar el rendimiento de monitorizar 2 variables, por ejemplo ”a” y ”b”:


$scope.$watch("a",function(newValue,oldValue) {
});
  
$scope.$watch("b",function(newValue,oldValue) {
});

Se puede hacer de la siguiente manera para mejorar el rendimiento al hacer un solo $watch:

$scope.$watch("a + b",function(newValue,oldValue) {
});

De esa forma solo hay un $watch en vez de dos. Esto tiene un problema ya que en aunque solo hay un $watch y mejoramos el rendimiento, hay condiciones que harán que no funcione correctamente. Veamos un ejemplo en el que no funcionaría:

app.controller("PruebaController", function($scope) {
 
  $scope.a=5;
  $scope.b=1;  
 
  $scope.$watch('a + b', function(newValue, oldValue) {
    if (newValue === oldValue) {
      return;
    }
 
    alert("Modificado");
  });
 
  $scope.change = function() {
    $scope.a=6;
    $scope.b=0;
  }

});

  • Línea 6: Se monitorizan las propiedades “a” y “b” mediante la expresión a + b
  • Línea 14: Se modifican las 2 propiedades a la vez.

En este caso nunca se llamará a la función de callback ya que el antiguo valor de la expresión es 6 al ser la suma de 5 + 1 2), pero el nuevo valor también es 6 al ser la suma de 6 + 0 3) , por lo que nunca se detectará el cambio en estos casos. Es decir que la expresión ”a + b” significa monitorizar la SUMA de ”a” y ”b” por lo que si dicha suma no cambia nunca se ejecuta la función de callback.

Ejemplo

Ejemplo simple del método $watch

Referencias

1) comprueba como funciona la función angular.equals ya que por ejemplo un NaN es igual para ella pero en JavaScript no
2) a + b = 5 + 1 = 6
3) a + b = 6 + 0 = 6
unidades/11_rootscope/01_watch.txt · Última modificación: 2014/10/25 10:26 por admin
Ir hasta arriba
CC Attribution-Share Alike 3.0 Unported
chimeric.de = chi`s home Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0