Introdução
O escopo está em uma posição central no ecossistema NG. A camada subjacente da ligação bidirecional reivindicada por Ng ao mundo exterior é realmente implementada pelo escopo. Este capítulo analisa principalmente o mecanismo de relógio do escopo, a herança e a implementação de eventos.
monitor
1. $ Relógio
1.1 Use
// $ relógio: function (watchExp, ouvinte, objectEquality)
var unwatch = $scope.$watch('aa', function () {}, isEqual);
Aqueles que usaram o Angular geralmente usam o código acima, conhecido como "manualmente", adicionando escuta, e alguns outros ouvintes adicionam automaticamente a audição através da interpolação ou diretiva, mas, em princípio, são os mesmos.
1.2 Análise do código -fonte
function (watchExp, ouvinte, objectEquality) {var scope = this, // compilam seqüências possíveis em fn get = compileTofn (watchExp, 'watch'), array = scope. $$ watchers, watcher = {fn: lister, último: initwal comparação ou comparação de valor}; lastdirtywatch = null; if (! isfunction (ouvinte)) {var listenfn = CompileTofn (ouvinte || Noop, 'ouvinte'); watchter.fn = function (newVal, OldVal, Scope) {Listenfn (escopo);}; } if (! Array) {Array = SCOPE. $$ WISTHERS = []; } // O motivo pelo qual o não é pressionado o não é pressionado é porque, em $ Digest, o loop dos observadores começa a partir do verso/ para permitir que o observador recém -adicionado seja executado no loop atual, é colocado na frente da fila.unshift (Watcher); // Retornar UNUCLATCHFN, CANCELAR RETURN FUNÇÃO DEREGISTERWATCH () {ArrayRemove (Array, Watcher); lastdirtywatch = null; };}Do código, o $ relógio é relativamente simples. Isso salva principalmente o observador na matriz $$ Watchers
2. $ Digest
Quando o valor do escopo muda, o escopo não executa cada ouvinte de observador por si só. Deve haver uma notificação, e quem envia esta notificação é $ DIGEST
2.1 Análise de código -fonte
O código -fonte de toda a $ Digest é de cerca de 100 linhas e a lógica principal está concentrada no loop de verificação sujo. Existem também alguns códigos menores após o loop, como o processamento do pós -Digestqueue, para que não analisaremos em detalhes.
Loop de verificação de valor sujo significa que, desde que haja uma atualização para o valor de um observador, uma rodada de cheques deve ser executada até que nenhuma atualização de valor seja atualizada. Obviamente, algumas otimizações foram feitas para reduzir verificações desnecessárias.
Código:
// Digite o loop $ Digest e marque -o para impedir a entrada repetida no início ('$ digery'); lastDirtyWatch = null; // o loop de verificação do valor sujo inicia do {Dirty = false; atual = destino; // O loop assíncrono omite o TraversCopeSloop: Do {if ((Watchers = Current. $$ Watchers)) {Length = Watchers.Length; while (comprimento--) {tente {watch = Watchers [comprimento]; Se (relógio) {// Faça uma atualização para determinar se há um valor atualizado, decomponha a seguinte // value = watch.get (atual), last = watch.last // value! == último se for verdadeiro, então determine se não for necessário para fazer um valor para julgar. watch.get (atual))! == (last = watch.last) &&! (watch.eq? Equals (valor, último): (typeof value === 'número' && typeof last === 'número' && isnan (valor) && isnan (last)))) {Dirty = true; // registra que o relógio muda neste loop lastdirtywatch = watch; // cache Último valor watch.last = watch.eq? cópia (valor, nulo): value; // Executa o ouvinte (newValue, lastValue, escopo) // Se a primeira execução for realizada, o LastValue também será definido como newValue watch.fn (valor, ((last === initwatchVal)? // ... Watchlog omite se (watch.get. $$ UNLATCH) estábulowatchescandidates.push ({assista: assista, array: Watchers}); } // Esta é a otimização para reduzir o observador // Se o último relógio atualizado no loop anterior não tiver alterado, ou seja, não há um novo relógio atualizado nesta rodada // então significa que os relógios inteiros foram estáveis e não serão atualizados e o loop termina aqui. Os relógios restantes não precisam ser verificados, senão (Watch === LastDirtyWatch) {Dirty = false; Break Traversescopesloop; }}} catch (e) {clearPhase (); $ ExceptionHandler (e); }}} // Esta seção está um pouco emaranhada, o que está na verdade percebendo a primeira traseira // a-> [b-> d, c-> e] // Ordem de execução a, b, d, c, e // pega o primeiro filho de cada vez. Se não houver um irmão próximo, se não houver irmão, recue para a camada anterior e determine se há irmãos na camada. Se não houver, continue a recuar até que ele se retire para o escopo inicial. Neste momento, próximo == NULL, então o loop do escopo será excitado se (! (Próximo = (atual. }}} while ((atual = a seguir)); // quebrar o TraversCopeSloop vai diretamente aqui // Determine se ainda está em um loop de valor sujo e excedeu o número máximo de verificações TTL padrão 10 if ((suty || asyncQueue.length) &&! (ttl--)) {clearPhase (); LONGE $ ROOTSCOPEMINERR ('Infdig', '{0} $ DIGEST () iterações alcançadas. Abortando!/N' + 'Vigilantes disparados nas últimas 5 iterações: {1}', TTL, TOJSON (Watchlog)); }} while (sujo || assyncqueue.length); // Fim do loop // Mark Sair Digest Loop ClearPhase ();Existem loops de 3 camadas no código acima
A primeira camada julga suja, se houver um valor sujo, continue a fazer um loop
fazer {
// ...
} while (sujo)
A segunda camada determina se o escopo foi atravessado. O código foi traduzido. Embora ainda esteja divulgado, pode ser entendido.
fazer {
// ....
if (atual. $$ Childhead) {
Próximo = atual. $$ Childhead;
} else if (atual! == Target && Current. $$ NextSibling) {
Próximo = atual. $$ NEXTSibling;
}
while (! Next && Current! == Target &&! (Next = Current. $$ NextSibling)) {
atual = atual. $ pai;
}
} while (atual = a seguir);
A terceira camada de observadores de escopo de loop
comprimento = observador.length;
while (comprimento--) {
tentar {
Watch = Watchers [comprimento];
// ... omitido
} catch (e) {
clearPhase ();
$ ExceptionHandler (e);
}
}
3. $ EVALASSYNC
3.1 Análise de código fonte
$ avalaSync é usado para execução atrasada, o código -fonte é o seguinte:
function (expr) {if (! } this. $$ asyncQueue.push ({scope: this, expressão: expr});}Julgando se o cheque sujo já está em execução ou alguém acionou $ avalaSync
if (! $ rootscope. $$ fase &&! $ rootscope. $$ assyncqueue.length) $ navegador.defer é alterar a ordem de execução chamando o setTimeout $ Browser.defer (function () {// ...});Se você não está usando adiar, então
function (exp) {fileue.push ({scope: this, expressão: exp}); este.O conteúdo do Async foi omitido na seção anterior $ Digest, localizado na primeira camada de loop
while (asyncQueue.length) {tente {asynctask = asyncQueue.shift (); asynctask.scope. $ EVAL (assynctask.expression); } catch (e) {clearPhase (); $ ExceptionHandler (e); } lastdirtywatch = null;}Simples e fácil de entender, retire a assíncada para execução.
Mas há um detalhe aqui, por que está configurado assim? O motivo é o seguinte. Se um novo assíncrono for adicionado quando o WatchX for executado em um determinado loop, LastDirtyWatch = watchx será definido neste momento. A execução desta tarefa fará com que um novo valor seja executado no relógio subsequente do WatchX. Se não houver código a seguir, o loop pulará do próximo loop para o LastDirtyWatch (WatchX) e sujo == Falso neste momento.
lastdirtywatch = null;
Há também um detalhe aqui, por que fazer loop no primeiro nível? Como o escopo com o relacionamento de herança possui $$ assíncrono, tudo é montado na raiz, por isso não precisa ser executado na camada de escopo da próxima camada.
2. Herança
O escopo é herdado, como $ PESOESCOPE e $ ChildsCope, dois escopos. Quando $ ChildsCope.fn é chamado Se não houver método FN em $ ChildsCope, vá para $ Parentscope para encontrar o método.
Pesquise a camada por camada por camada até encontrar o atributo necessário. Esse recurso é implementado usando as características da herança do protótipo de JavaScipt.
Código -fonte:
função (isolate) {var ChildScope, filho; if (isolado) {filho = new Scope (); criança. $ root = this. $ root; // O assíncico e o pós -digestão de isolado também são raiz pública e outras crianças independentes. $$ assyncqueue = this. $$ assíncrono; filho. } else {if (! this. $$ ChildScopeClass) {this. $$ ChildScoPEClass = function () {// Aqui podemos ver quais atributos são exclusivos para isolar, como os observadores de $$, para que sejam monitorados independentemente. This. $$$ Watchers = this. this. $$ ouvintes = {}; este. $$ ouvidos outCount = {}; isto. $ id = nextUid (); Este. $$ ChildScopeClass = null; }; este. $$ ChildScopeClass.prototype = this; } criança = novo this. $$ ChildSCOPECLASS (); } // Defina vários relacionamentos de pai e filho e irmão, o que é muito confuso! criança ['this'] = filho; filho. $ pai = this; criança. $$ Prevesibling = This. if (this. $$ childhead) {this. $$ rabo de criança. $$ NEXTSibling = criança; Este. $$ Childtail = criança; } else {this. $$ childhead = this. $$ Childtail = criança; } retornar criança;}O código é claro, os principais detalhes são quais atributos precisam ser independentes e quais precisam ser baseados no básico.
O código mais importante:
this.$$childScopeClass.prototype = this;
A herança é realizada dessa maneira.
3. Mecanismo de eventos
3.1 $ on
function (nome, ouvinte) {var nomeadoListeners = this. $$ ouvintes [nome]; if (! nomeadoListeners) {this. $$ ouvintes [nome] = nomeadoListeners = []; } nomeadoListeners.push (ouvinte); Var Current = this; do {if (! atualmente. $$ ouvido a parte de ouvidos [name]) {atual. $$ ouvido outerCount [nome] = 0; } atualização. $$ ouvidos outCount [nome] ++; } while ((current = current. $ pai)); var self = this; Return function () {nomeadoListeners [indexof (chamadoListeners, ouvinte)] = null; decrementListenerCount (self, 1, nome); };}Semelhante ao $ WATHC, ele também é armazenado em uma matriz - nomeada.
Há outra diferença de que o escopo e todos os pais salvam estatísticas de eventos, que são úteis ao transmitir eventos e análises subsequentes.
var atual = this; do {if (! atualmente. $$ outerCount [name]) {atual. $$ ouvido outercount [nome] = 0; } atualização. $$ ouvidos outCount [nome] ++;} while ((atual = current. $ pai));3.2 $ emit
$ emit é um evento de transmissão ascendente. Código -fonte:
function (nome, args) {var em vazio = [], nomeadoListeners, scope = this, stoppropagation = false, event = {nome: nome, destino: escopo, stopPropagation: function () {stopPropagation = true; }. do {nomeadoListeners = scope. $$ ouvintes [nome] || vazio; event.CurrentsCope = SCOPE; para (i = 0, comprimento = nomeadoListeners.Length; i <comprimento; i ++) {// Depois de ouvir remover, ele não será excluído da matriz, mas está definido como nulo, por isso é necessário julgar se (! chamado Listeners [i]) {nomeadoListeners.splice (i, 1); eu--; comprimento--; continuar; } tente {nomeadoListeners [i] .Apply (null, listeRargs); } catch (e) {$ excepcionHandler (e); }} // Quando a propagação é interrompida, retorne se (stopPropagation) {event.currentscope = null; evento de retorno; } // emit é a maneira de propagar o escopo ascendente = escopo. $ Parent; } while (escopo); Event.CurrentsCope = NULL; evento de retorno;}3,3 $ transmissão
$ transmitido está propagando para dentro, isto é, propagando para a criança, código -fonte:
function (nome, args) {var no destino = this, current = destino, next = alvo, evento = {nome: nome, destino: destino, prevenDDefault: function () {event.DefaultPreventEd = true; }. while ((Current = Next)) {Event.CurrentsCope = Current; ouvintes = atual. $$ ouvintes [nome] || []; para (i = 0, comprimento = ouvintes.length; i <comprimento; i ++) {// Verifique se a audição foi cancelada se (! ouvintes [i]) {ouvintes.splice (i, 1); eu--; comprimento--; continuar; } tente {ouvintes [i] .Apply (nulo, ouvintes); } catch (e) {$ excepcionHandler (e); }} // if (next = ((atual. $$ outerCount [nome] && Current. $$ childhead) || (atual! == Target && Current. $$ NextSibling)))) {while (Current! }}} event.currentscope = null; evento de retorno;}A outra lógica é relativamente simples, ou seja, o código que é atravessado em profundidade é mais confuso. De fato, é o mesmo que no Digest. É para julgar se há uma escuta no caminho. Atual. $$ Ouvir CARRERCOUNT [Nome]. A partir do Código de $ ON, podemos ver que, desde que haja uma criança no caminho e ouça, o cabeçalho do caminho também possui um número. Pelo contrário, se não for afirmado que todas as crianças no caminho não têm eventos de escuta.
if (! (próximo = ((atualização. }}
Caminho de propagação:
Raiz> [a> [a1, a2], b> [b1, b2> [c1, c2], b3]]
Raiz> a> a1> a2> b> b1> b2> c1> c2> b3
4. $ Watchcollection
4.1 Use exemplos
$ scope.names = ['igor', 'matias', 'misko', 'james']; $ scope.datacount = 4; $ scope. $ watchcollection ('nomes', function (newNames, OldNames) {$ scope.datacount = newnames.length;}); espera ($ scope.datacount) .ToEqual (4); $ SCOPE. $ DIGEST (); espera ($ SCOPE.DATACOUNT).4.2 Análise de código -fonte
function (obj, ouvinte) {$ watchCollectionIntercept. $ stateful = true; var self = this; var newValue; Var OldValue; varyoldValue; var trackveryoldValue = (ouvinte.length> 1); var alternoutectect = 0; var alteração dotector = $ parse (obj, $ watchcollectionInterceptor); var internalArray = []; var internalObject = {}; var initrun = true; var de OldLength = 0; // Determine se a mudança deve ser alterada com base na função alterada retornada $ watchcollectionIntercept (_value) {// ... return alternoutected; } // Ligue para o ouvinte real através deste método como uma função proxy $ watchCollectionAction () {} retorna este. $ Watch (alteraçãoA veia principal faz parte do código interceptado acima. O seguinte analisa principalmente $ WatchCollectionInterceptor e $ WatchCollectionAction
4.3 $ WatchCollectionIntercept
função $ watchcollectionIntercept (_value) {newValue = _value; Var NewLength, Key, Bothnan, NewItem, OldItem; if (isundefined (newvalue)) retornar; if (! isObject (newValue)) {if (OldValue! == newValue) {OldValue = newValue; alterado o ++; }} else if (isarraylike (newValue)) {if (OldValue! == InternalArray) {OldValue = internalArray; OldLength = OldValue.Length = 0; alterado o ++; } newLength = newValue.length; if (antigo! OldValue.Length = OldLength = newLength; } para (var i = 0; i <newLength; i ++) {antigoTem = antiga [i]; newItem = newValue [i]; Bothnan = (OldItem! == OldItem) && (newItem! == newItem); if (! Bothnan && (antigo! == newItem)) {alterado. OldValue [i] = newItem; }}} else {if (OldValue! == InternalObject) {OldValue = internalObject = {}; OldLength = 0; alterado o ++; } newLength = 0; para (key in newValue) {if (histownsProperty.call (newValue, chave)) {newLength ++; newItem = newValue [chave]; OldItem = OldValue [chave]; if (key in OldValue) {bothnan = (OldItem! == OldItem) && (newItem! == newItem); if (! Bothnan && (antigo! == newItem)) {alterado. OldValue [key] = newItem; }} else {OldLength ++; OldValue [key] = newItem; alterado o ++; }}} if (OldLength> newLength) {alterado para (key in OldValue) {if (! hasOwnProperty.Call (newValue, key)) {OldLength--; excluir OldValue [chave]; }}}} Return alternoutected;}1). Retorne diretamente quando o valor estiver indefinido.
2). Quando o valor for um tipo básico comum, determine diretamente se é igual.
3). Quando o valor é uma matriz de classe (ou seja, o atributo de comprimento existe e o valor [i] também é chamado de matriz de classe), o OldValue é inicializado primeiro sem inicialização
if (OldValue! == InternalArray) {OldValue = internalArray; OldLength = OldValue.Length = 0; Alterado ++;}Em seguida, compare o comprimento da matriz, se não for igual, é registrado como alteração alterada.
if (antigo! OldValue.Length = OldLength = newLength;}
Compare um por um
for (var i = 0; i <newLength; i ++) {antigo = OldValue [i]; newItem = newValue [i]; Bothnan = (OldItem! == OldItem) && (newItem! == newItem); if (! Bothnan && (antigo! == newItem)) {alterado. OldValue [i] = newItem; }}4). Quando o valor é objeto, o processo de inicialização é semelhante ao acima
if (OldValue! == InternalObject) {OldValue = internalObject = {}; OldLength = 0; Alterado ++;}O próximo processamento é mais hábil. Se você achar que novos campos com muitos novos valores são adicionados, adicione 1 ao comprimento antigo, para que o comprimento antigo apenas adicione e não subtraia. É fácil descobrir se existem novos campos em NewValue. Por fim, remova os campos extras em OldValue, ou seja, os campos excluídos em NewValue e depois acabou.
newLength = 0; para (key in newValue) {if (histownsProperty.call (newValue, key)) {newLength ++; newItem = newValue [chave]; OldItem = OldValue [chave]; if (key in OldValue) {bothnan = (OldItem! == OldItem) && (newItem! == newItem); if (! Bothnan && (antigo! == newItem)) {alterado. OldValue [key] = newItem; }} else {OldLength ++; OldValue [key] = newItem; alterado o ++; }}} if (OldLength> newLength) {alterado para (key in OldValue) {if (! hasOwnProperty.Call (newValue, key)) {OldLength--; excluir OldValue [chave]; }}}4.4 $ WatchCollectionAction
função $ watchCollectionAction () {if (initrun) {initrun = false; ouvinte (newvalue, newvalue, self); } else {ouvinte (newValue, muitololdValue, self); } // TrackVeLODOLDValue = (ouvinte.Length> 1) Verifique se o método do ouvinte requer OldValue // copie se necessário se (rastrearveryoldValue) {if (! isObject (newValue)) {muitololdValue = newValue; } else if (isarray -like (newvalue)) {muitooldValue = new Array (newvalue.length); for (var i = 0; i <newValue.length; i ++) {muitololdValue [i] = newValue [i]; }} else {muitololdValue = {}; for (var tecla var no newValue) {if (hasOwnProperty.call (newValue, key)) {muitololdValue [key] = newValue [key]; }}}}}}O código é relativamente simples, é para chamar o ouvinte. Quando a primeira chamada é OldValue == NewValue. Para eficiência e memória, é determinado se o ouvinte precisa de parâmetro antigo.
5. $ avaliar & $ aplicar
$ avaliativo: function (expr, habitantes locais) {return $ parse (expr) (this, habitantes locais);}, $ apply: function (expr) {try {BeginPhase ('$ aplicar'); retornar isso. $ EVAL (EXPR); } catch (e) {$ excepcionHandler (e); } finalmente {clearPhase (); tente {$ Rootscope. $ DIGEST (); } catch (e) {$ excepcionHandler (e); jogar e; }}}$ Aplicar finalmente chama $ RootsCope. $ DIGEST (), muitos livros recomendam o uso de $ Digest () em vez de chamar $ APLAP (), o que é mais eficiente.
A lógica principal é tudo em $ parse, que pertence à função de análise de sintaxe e será analisado separadamente no futuro.