Como funciona a binding de dados no AngularJS?

Como a binding de dados funciona no framework AngularJS ?

Não encontrei detalhes técnicos em seu site . É mais ou menos claro como isso funciona quando os dados são propagados da visualização para o modelo. Mas como o AngularJS controla as alterações das propriedades do modelo sem setters e getters?

Eu descobri que existem observadores de JavaScript que podem fazer esse trabalho. Mas eles não são suportados no Internet Explorer 6 e no Internet Explorer 7 . Então, como o AngularJS sabe que eu mudei por exemplo o seguinte e refletiu essa mudança em uma visão?

 myobject.myproperty="new value"; 

AngularJS lembra o valor e o compara com um valor anterior. Esta é uma verificação suja básica. Se houver uma alteração no valor, ele acionará o evento de mudança.

O método $apply() , que é o que você chama quando está fazendo a transição de um mundo não AngularJS para um mundo AngularJS, chama $digest() . Um resumo é simplesmente uma velha sujeira. Funciona em todos os navegadores e é totalmente previsível.

Para contrastar a verificação suja (AngularJS) vs alterar os ouvintes ( KnockoutJS e Backbone.js ): Embora a verificação de sujeira possa parecer simples e até ineficiente (vou abordar isso mais tarde), verifica-se que ela está semanticamente correta o tempo todo, enquanto os ouvintes de mudança têm muitos casos esquisitos e precisam de coisas como rastreamento de dependência para torná-lo mais semanticamente correto. O rastreamento de dependência do KnockoutJS é um recurso inteligente para um problema que o AngularJS não possui.

Problemas com ouvintes de mudança:

  • A syntax é atroz, já que os navegadores não suportam nativamente. Sim, existem proxies, mas eles não são semanticamente corretos em todos os casos e, obviamente, não há proxies em navegadores antigos. O importante é que a verificação suja permite que você faça POJO , enquanto KnockoutJS e Backbone.js forçam você a herdar de suas classs e acessar seus dados por meio de acessadores.
  • Mude a coalescência. Suponha que você tenha uma matriz de itens. Digamos que você queira adicionar itens em uma matriz, como você está dando um loop para adicionar, toda vez que você adicionar, você está triggersndo events em change, que está renderizando a UI. Isso é muito ruim para o desempenho. O que você quer é atualizar a interface do usuário apenas uma vez, no final. Os events de alteração são muito granulares.
  • Alterar os ouvintes triggersm imediatamente em um setter, o que é um problema, já que o listener de mudança pode alterar ainda mais os dados, o que triggers mais events de mudança. Isso é ruim, já que na sua pilha você pode ter vários events de mudança acontecendo ao mesmo tempo. Suponha que você tenha dois arrays que precisam ser mantidos em sincronia por qualquer motivo. Você só pode adicionar um ou outro, mas cada vez que você adicionar você triggers um evento de mudança, que agora tem uma visão inconsistente do mundo. Esse é um problema muito semelhante ao bloqueio de encadeamento, que o JavaScript evita, já que cada retorno de chamada é executado exclusivamente e até a conclusão. Os events de mudança quebram isso, pois os setters podem ter consequências de longo alcance que não são intencionais nem óbvias, o que cria o problema de thread novamente. Acontece que o que você quer fazer é atrasar a execução do ouvinte e garantir que apenas um ouvinte seja executado por vez, portanto, qualquer código está livre para alterar dados e ele sabe que nenhum outro código é executado enquanto estiver fazendo isso. .

O que sobre o desempenho?

Então, pode parecer que estamos lentos, já que a verificação suja é ineficiente. É aqui que precisamos olhar números reais em vez de apenas argumentos teóricos, mas primeiro vamos definir algumas restrições.

Humanos são:

  • Lento – Qualquer coisa mais rápida que 50 ms é imperceptível para humanos e assim pode ser considerada como “instantânea”.

  • Limitado – Você não pode exibir mais de 2000 informações para um ser humano em uma única página. Qualquer coisa além disso é uma interface realmente ruim, e humanos não podem processar isso de qualquer maneira.

Então a verdadeira questão é esta: quantas comparações você pode fazer em um navegador em 50 ms? Essa é uma pergunta difícil de responder, já que muitos fatores entram em jogo, mas aqui está um caso de teste: http://jsperf.com/angularjs-digest/6, que cria 10.000 observadores. Em um navegador moderno, isso leva pouco menos de 6 ms. No Internet Explorer 8 , leva cerca de 40 ms. Como você pode ver, isso não é um problema, mesmo em navegadores lentos nos dias de hoje. Há uma ressalva: as comparações precisam ser simples para caber no limite de tempo … Infelizmente, é muito fácil adicionar uma comparação lenta no AngularJS, por isso é fácil criar aplicativos lentos quando você não sabe o que estão fazendo. Mas esperamos ter uma resposta fornecendo um módulo de instrumentação, que mostraria quais são as comparações lentas.

Acontece que videogames e GPUs usam a abordagem de verificação suja, especificamente porque é consistente. Contanto que eles superem a taxa de atualização do monitor (normalmente 50-60 Hz, ou a cada 16,6-20 ms), qualquer desempenho acima disso é um desperdício, então é melhor tirar mais material do que obter FPS mais alto.

Misko já deu uma excelente descrição de como as ligações de dados funcionam, mas eu gostaria de adicionar minha opinião sobre o problema de desempenho com a binding de dados.

Como Misko afirmou, cerca de 2000 ligações é onde você começa a ver problemas, mas você não deve ter mais de 2.000 informações em uma página de qualquer maneira. Isso pode ser verdade, mas nem toda vinculação de dados é visível para o usuário. Uma vez que você comece a construir qualquer tipo de widget ou grade de dados com binding bidirecional, você pode facilmente acertar ligações de 2000, sem ter um ux ruim.

Considere, por exemplo, uma checkbox de combinação na qual você pode digitar texto para filtrar as opções disponíveis. Esse tipo de controle pode ter ~ 150 itens e ainda ser altamente utilizável. Se tiver algum recurso extra (por exemplo, uma class específica na opção atualmente selecionada), você começará a obter de 3 a 5 ligações por opção. Coloque três desses widgets em uma página (por exemplo, um para selecionar um país, outro para selecionar uma cidade no país em questão e o terceiro para selecionar um hotel) e você já está entre as ligações de 1000 e 2000.

Ou considere uma grade de dados em um aplicativo da Web corporativo. 50 linhas por página não é irracional, cada uma delas com 10 a 20 colunas. Se você construir isto com ng-repeats, e / ou tiver informações em algumas células que usam algumas ligações, você poderia estar se aproximando de 2000 ligações somente com essa grade.

Acho que isso é um problema enorme quando se trabalha com o AngularJS, e a única solução que consegui encontrar até agora é construir widgets sem usar binding bidirecional, usando o ngOnce, desregistrando observadores e truques similares, ou construindo diretivas. que constrói o DOM com manipulação de jQuery e DOM. Eu sinto que isso acaba com o propósito de usar Angular em primeiro lugar.

Eu adoraria ouvir sugestões sobre outras maneiras de lidar com isso, mas talvez eu devesse escrever minha própria pergunta. Eu queria colocar isso em um comentário, mas acabou sendo muito longo para isso …

TL; DR
A binding de dados pode causar problemas de desempenho em páginas complexas.

Por sujo verificar o object $scope

O Angular mantém uma array simples de observadores nos objects $scope . Se você inspecionar qualquer $scope verá que ele contém uma array chamada $$watchers .

Cada observador é um object que contém entre outras coisas

  1. Uma expressão que o observador está monitorando. Isso pode ser apenas um nome de attribute ou algo mais complicado.
  2. Um último valor conhecido da expressão. Isso pode ser verificado em relação ao valor computado atual da expressão. Se os valores diferirem, o inspetor acionará a function e marcará o $scope como sujo.
  3. Uma function que será executada se o observador estiver sujo.

Como os observadores são definidos

Existem muitas maneiras diferentes de definir um observador no AngularJS.

  • Você pode explicitamente $watch um attribute no $scope .

     $scope.$watch('person.username', validateUnique); 
  • Você pode colocar uma interpolação {{}} no seu modelo (um observador será criado para você no $scope atual do $scope ).

     

    username: {{person.username}}

  • Você pode pedir uma diretiva como ng-model para definir o observador para você.

      

O ciclo $digest verifica todos os observadores em relação ao último valor

Quando interagimos com o AngularJS através dos canais normais (ng-model, ng-repeat, etc), um ciclo de digitação será acionado pela diretiva.

Um ciclo de digitação é uma travessia em profundidade de $scope e todos os seus filhos . Para cada object $scope , nós fazemos iterações em sua array $$watchers e avaliamos todas as expressões. Se o novo valor de expressão for diferente do último valor conhecido, a function do inspetor é chamada. Essa function pode recompilar parte do DOM, recalcular um valor em $scope , acionar uma request AJAX , qualquer coisa que você precise fazer.

Cada escopo é percorrido e cada expressão de relógio é avaliada e verificada em relação ao último valor.

Se um observador for acionado, o $scope estará sujo

Se um observador for acionado, o aplicativo saberá que algo foi alterado e o $scope será marcado como sujo.

As funções do inspetor podem alterar outros atributos no $scope ou em um $scope pai $scope . Se uma function $watcher foi ativada, não podemos garantir que nossos outros $scope ainda estejam limpos e, portanto, executamos todo o ciclo de digitação novamente.

Isso ocorre porque o AngularJS possui uma binding bidirecional, portanto, os dados podem ser repassados ​​pela tree $scope . Podemos alterar um valor em um $scope mais alto que já tenha sido digerido. Talvez nós $rootScope um valor no $rootScope .

Se o $digest estiver sujo, executamos todo o ciclo $digest novamente

Continuamente percorremos o ciclo $digest até que o ciclo de digitação seja limpo (todas as expressões de $watch têm o mesmo valor que tinham no ciclo anterior), ou chegamos ao limite de digest. Por padrão, esse limite é definido como 10.

Se atingirmos o limite de resumo, o AngularJS irá gerar um erro no console:

 10 $digest() iterations reached. Aborting! 

O resumo é difícil na máquina, mas fácil para o desenvolvedor

Como você pode ver, toda vez que algo muda em um aplicativo AngularJS, o AngularJS verifica cada observador na hierarquia $scope para ver como responder. Para um desenvolvedor, este é um grande benefício de produtividade, já que agora você não precisa escrever quase nenhum código de conexão, o AngularJS apenas notará se um valor foi alterado e tornará o resto do aplicativo consistente com a mudança.

Do ponto de vista da máquina, isso é extremamente ineficiente e diminuirá a velocidade do aplicativo se criarmos muitos observadores. Misko citou um número de cerca de 4.000 espectadores antes que o seu aplicativo pareça lento em navegadores mais antigos.

Esse limite é fácil de alcançar se você ng-repeat em um grande array JSON por exemplo. Você pode atenuar isso usando resources como binding única para compilar um modelo sem criar observadores.

Como evitar a criação de muitos espectadores

Cada vez que seu usuário interage com seu aplicativo, todos os observadores do seu aplicativo serão avaliados pelo menos uma vez. Uma grande parte da otimização de um aplicativo AngularJS está reduzindo o número de observadores na sua tree $scope . Uma maneira fácil de fazer isso é com uma binding de tempo .

Se você tem dados que raramente mudam, você pode ligá-lo apenas uma vez usando a syntax ::, assim:

 

{{::person.username}}

ou

 

A binding só será acionada quando o modelo contido for renderizado e os dados carregados no $scope .

Isso é especialmente importante quando você faz uma ng-repeat com muitos itens.

 
{{::person.username}}

Este é meu entendimento básico. Pode muito bem estar errado!

  1. Itens são vistos passando uma function (retornando a coisa a ser observada) para o método $watch .
  2. As alterações nos itens observados devem ser feitas dentro de um bloco de código agrupado pelo método $apply .
  3. No final do $apply o método $digest é invocado, passando por cada um dos relógios e verificando se eles foram alterados desde a última vez que o $digest executado.
  4. Se alguma alteração for encontrada, o resumo será chamado novamente até que todas as alterações se estabilizem.

No desenvolvimento normal, a syntax de vinculação de dados no HTML informa ao compilador AngularJS para criar os relógios para você e os methods do controlador são executados dentro de $apply já. Então, para o desenvolvedor de aplicativos, tudo é transparente.

Eu me perguntei por um tempo. Sem setters, como o AngularJS percebe mudanças no object $scope ? Será que os enquete?

O que ele realmente faz é o seguinte: qualquer lugar “normal” que você modifique o modelo já foi chamado da coragem do AngularJS , por isso ele automaticamente chama $apply para você depois que seu código é executado. Digamos que seu controlador tenha um método conectado a algum ng-click . Como o AngularJS a chamada desse método para você, ele tem a chance de fazer um $apply no local apropriado. Da mesma forma, para expressões que aparecem diretamente nas visualizações, elas são executadas pelo AngularJS portanto, ele aplica o $apply .

Quando a documentação fala sobre ter que chamar $apply manualmente para código fora do AngularJS , está falando sobre o código que, quando executado, não se AngularJS próprio AngularJS na pilha de chamadas.

Explicando com Imagens:

A vinculação de dados precisa de um mapeamento

A referência no escopo não é exatamente a referência no modelo. Quando você vincula dois objects a dados, você precisa de um terceiro que ouça o primeiro e modifique o outro.

insira a descrição da imagem aqui

Aqui, quando você modifica o , você toca no data-ref3 . E o mecanismo clássico de vinculação de dados mudará data-ref4 . Então, como as outras expressões {{data}} vão se mover?

Eventos leva a $ digest ()

insira a descrição da imagem aqui

Angular mantém um oldValue e newValue de cada binding. E após cada evento Angular , o famoso loop $digest() irá verificar a WatchList para ver se algo mudou. Esses events angulares são ng-click , ng-change , $http concluído … O $digest() entrará em loop contanto que qualquer oldValue diferente do newValue .

Na figura anterior, ele notará que data-ref1 e data-ref2 foram alterados.

Conclusões

É um pouco como o ovo e o frango. Você nunca sabe quem começa, mas esperamos que funcione na maior parte do tempo como esperado.

O outro ponto é que você pode entender facilmente o impacto profundo de uma binding simples na memory e na CPU. Esperançosamente Desktops são gordos o suficiente para lidar com isso. Os telefones celulares não são tão fortes.

Obviamente, não há verificação periódica do Scope se há alguma alteração nos objects anexados a ele. Nem todos os objects anexados ao escopo são observados. O escopo mantém protótipos de $ $$ . Scope apenas repete os $$watchers quando $digest é chamado.

Angular adiciona um observador aos observadores de $ $ para cada um destes

  1. {{expression}} – Em seus modelos (e em qualquer outro lugar onde haja uma expressão) ou quando definimos ng-model.
  2. $ scope. $ watch (‘expressão / function’) – Em seu JavaScript, podemos append um object de escopo para observar angularmente.

A function $ watch recebe três parâmetros:

  1. A primeira é uma function watcher que apenas retorna o object ou podemos simplesmente adicionar uma expressão.

  2. O segundo é uma function de ouvinte que será chamada quando houver uma alteração no object. Todas as coisas como alterações DOM serão implementadas nesta function.

  3. O terceiro é um parâmetro opcional que aceita um booleano. Se o seu verdadeiro, angular profundo observa o object e se o seu falso Angular só faz uma referência observando no object. Implementação aproximada de $ watch parece com isso

 Scope.prototype.$watch = function(watchFn, listenerFn) { var watcher = { watchFn: watchFn, listenerFn: listenerFn || function() { }, last: initWatchVal // initWatchVal is typically undefined }; this.$$watchers.push(watcher); // pushing the Watcher Object to Watchers }; 

Há uma coisa interessante em Angular chamado Digest Cycle. O ciclo $ digest é iniciado como resultado de uma chamada para $ scope. $ Digest (). Suponha que você altere um modelo de escopo de $ em uma function de manipulador através da diretiva ng-click. Nesse caso, o AngularJS triggers automaticamente um ciclo $ digest chamando $ digest (). Além do ng-click, existem várias outras diretivas / serviços integrados que permitem a mudança de modelos (por exemplo, ng-model, $ timeout, etc) e triggersr automaticamente um ciclo $ digest. A implementação aproximada do $ digest parece com isso.

 Scope.prototype.$digest = function() { var dirty; do { dirty = this.$$digestOnce(); } while (dirty); } Scope.prototype.$$digestOnce = function() { var self = this; var newValue, oldValue, dirty; _.forEach(this.$$watchers, function(watcher) { newValue = watcher.watchFn(self); oldValue = watcher.last; // It just remembers the last value for dirty checking if (newValue !== oldValue) { //Dirty checking of References // For Deep checking the object , code of Value // based checking of Object should be implemented here watcher.last = newValue; watcher.listenerFn(newValue, (oldValue === initWatchVal ? newValue : oldValue), self); dirty = true; } }); return dirty; }; 

Se usarmos a function setTimeout () do JavaScript para atualizar um modelo de escopo, o Angular não terá como saber o que você pode alterar. Nesse caso, é nossa responsabilidade chamar $ apply () manualmente, o que aciona um ciclo $ digest. Da mesma forma, se você tiver uma diretiva que configure um ouvinte de evento DOM e altere alguns modelos dentro da function de manipulador, será necessário chamar $ apply () para garantir que as alterações entrem em vigor. A grande ideia de $ apply é que podemos executar algum código que não conhece Angular, esse código ainda pode mudar as coisas no escopo. Se nós envolvermos esse código em $ apply, ele será chamado de $ digest (). Implementação aproximada de $ apply ().

 Scope.prototype.$apply = function(expr) { try { return this.$eval(expr); //Evaluating code in the context of Scope } finally { this.$digest(); } }; 

O AngularJS manipula o mecanismo de vinculação de dados com a ajuda de três funções poderosas: $ watch () , $ digest () e $ apply () . Na maioria das vezes, o AngularJS chamará o $ scope. $ Watch () e $ scope. $ Digest (), mas em alguns casos você pode ter que chamar essas funções manualmente para atualizar com novos valores.

$ watch () : –

Essa function é usada para observar alterações em uma variável no escopo $. Aceita três parâmetros: expressão, ouvinte e object de igualdade, onde ouvinte e object de igualdade são parâmetros opcionais.

$ digest ()

Essa function repete todos os relógios no object $ scope e seus objects child $ scope
(se tiver algum). Quando $ digest () itera sobre os relógios, ele verifica se o valor da expressão foi alterado. Se o valor foi alterado, o AngularJS chama o ouvinte com novo valor e valor antigo. A function $ digest () é chamada sempre que o AngularJS achar necessário. Por exemplo, após um clique no botão ou após uma chamada AJAX. Você pode ter alguns casos em que o AngularJS não chama a function $ digest () para você. Nesse caso, você tem que ligar para você mesmo.

$ apply ()

O Angular atualiza automaticamente apenas as alterações do modelo que estão dentro do contexto do AngularJS. Quando você alterar qualquer modelo fora do contexto Angular (como events DOM do navegador, bibliotecas setTimeout, XHR ou de terceiros), será necessário informar o Angular sobre as alterações chamando $ apply () manualmente. Quando a chamada da function $ apply () termina chamadas do AngularJS $ digest () internamente, todas as ligações de dados são atualizadas.

Aconteceu que eu precisava vincular um modelo de dados de uma pessoa com um formulário, o que fiz foi um mapeamento direto dos dados com o formulário.

Por exemplo, se o modelo tivesse algo como:

 $scope.model.people.name 

A input de controle do formulário:

  

Dessa forma, se você modificar o valor do controlador de object, isso será refletido automaticamente na exibição.

Um exemplo onde eu passei o modelo é atualizado de dados do servidor é quando você pede um código postal e CEP com base em cargas escritas uma lista de colônias e cidades associadas a essa visão e, por padrão, definir o primeiro valor com o usuário. E isso eu trabalhei muito bem, o que acontece, é que angularJS vezes, leva alguns segundos para atualizar o modelo, para fazer isso, você pode colocar um spinner enquanto exibe os dados.

  1. A binding de dados unidirecional é uma abordagem em que um valor é retirado do modelo de dados e inserido em um elemento HTML. Não há como atualizar o modelo da exibição. É usado em sistemas de modelos clássicos. Esses sistemas ligam dados em apenas uma direção.

  2. A vinculação de dados em aplicativos angulares é a synchronization automática de dados entre os componentes de modelo e exibição.

A binding de dados permite tratar o modelo como a única fonte de verdade em seu aplicativo. A visão é uma projeção do modelo em todos os momentos. Se o modelo for alterado, a visualização reflete a alteração e vice-versa.

binding de dados:

O que é binding de dados?

Sempre que o usuário altera os dados na exibição, ocorre uma atualização dessa alteração no modelo de escopo e vice-versa.

Como isso é possível?

Resposta curta: Com a ajuda do ciclo digestivo.

Descrição: Angular js define o observador no modelo de escopo, que triggers a function de ouvinte se houver uma alteração no modelo.

 $scope.$watch('modelVar' , function(newValue,oldValue){ 

// Dom atualiza o código com novo valor

});

Então, quando e como a function watcher é chamada?

A function Watcher é chamada como parte do ciclo de digestão.

Digest ciclo é chamado automaticamente acionado como parte do angular js construído em diretivas / serviços como ng-modelo, ng-bind, $ timeout, ng-click e outros .. que permitem acionar o ciclo de digerir.

Função de ciclo de digestão:

 $scope.$digest() -> digest cycle against the current scope. $scope.$apply() -> digest cycle against the parent scope 

ou seja, $rootScope.$apply()

Nota: $ apply () é igual a $ rootScope. $ Digest () isso significa que a verificação suja começa diretamente da raiz ou do topo ou do escopo pai para todos os escopos child $ no aplicativo js angular.

Os resources acima funcionam nos navegadores IE para as versões mencionadas, apenas certificando-se de que seu aplicativo é aplicativo js angular, o que significa que você está usando o arquivo de script de estrutura angularjs mencionado na tag de script.

Obrigado.

Aqui está um exemplo de binding de dados com o AngularJS, usando um campo de input. Vou explicar mais tarde

Código HTML

 

{{watchInput}}

Código AngularJS

 myApp = angular.module ("myApp", []); myApp.controller("myCtrl", ["$scope", function($scope){ //Your Controller code goes here }]); 

Como você pode ver no exemplo acima, o AngularJS usa o ng-model para ouvir e observar o que acontece nos elementos HTML, especialmente nos campos de input . Quando algo acontece, faça alguma coisa. No nosso caso, o ng-model é ligado à nossa visão, usando a notação do bigode {{}} . O que quer que seja typescript dentro do campo de input é exibido na canvas instantaneamente. E essa é a beleza da vinculação de dados, usando o AngularJS em sua forma mais simples.

Espero que isto ajude.

Veja um exemplo de trabalho aqui no Codepen

AngularJs suporta binding de dados bidirecional .
Significa que você pode acessar os dados Ver -> Controlador e Controlador -> Ver

Por exemplo

1)

 // If $scope have some value in Controller. $scope.name = "Peter"; // HTML 
{{ name }}

O / P

 Peter 

Você pode vincular dados no ng-model Like: –
2)

  
{{ name }}

Aqui no exemplo acima, qualquer usuário de input vai dar, será visível na tag

.

Se quiser ligar a input de html ao controlador: –
3)

 

Aqui, se você quiser usar o name input no controlador,

 $scope.name = {}; $scope.registration = function() { console.log("You will get the name here ", $scope.name); }; 

ng-model liga nossa visão e a transforma em expressão {{ }} .
ng-model são os dados que são mostrados ao usuário na visualização e com os quais o usuário interage.
Por isso, é fácil vincular dados em AngularJs.

O Angular.js cria um observador para todos os modelos que criamos. Whenever a model is changed, an “ng-dirty” class is appeneded to the model, so the watcher will observe all models which have the class “ng-dirty” & update their values in the controller & vice versa.