AngularJSの作成:パート1-スコープとダイジェスト

Angularは、成熟した強力なJavaScriptフレームワークです。 非常に大きく、効果的に機能するために習得する必要がある多くの新しい概念に基づいています。 Angularに慣れているほとんどの開発者は、同じ困難に直面しています。 ダイジェスト機能は正確に何をしますか? ディレクティブを作成する方法は何ですか? サービスとプロバイダーの違いは何ですか?



Angularには非常に優れたドキュメントがあり、 サードパーティのリソースがたくさんあるという事実にもかかわらず、技術を細かく分割してその魔法を明らかにするよりも、技術を学ぶ良い方法はありません。



このシリーズの記事では、 AngularJSをゼロから再作成します。 これを段階的に一緒に行います。その間に、Angularの内部構造をより深く理解できます。



このシリーズの最初の部分では、スコープデバイス、および$ eval$ digest、および$ applyが実際にどのように機能するかを見ていきます。 Angularでデータの変更(ダーティチェック)をチェックするのは魔法のように思えますが、そうではありません-自分で確認できます。



準備する



プロジェクトソースはgithubで入手できますが、自分でコピーすることはお勧めしません。 代わりに、私はあなたがすべてを自分で、一歩ずつ実行し、コードをいじり、それを掘り下げることを主張します。 テキストではJS Binを使用しているため、ページを離れることなくコードを操作できます( -変換では、JS Binコードへのリンクのみが存在します)。



配列とオブジェクトを使用した低レベルの操作にはLo-Dashを使用します。 Angular自体はLo-Dashを使用しませんが、この目的のために、テンプレートの低レベルコードを可能な限り削除することは理にかなっています。 コード(アンダースコア)のどこでも( _ )、Lo-Dash関数が呼び出されます。



また、単純なチェックのためにconsole.assert関数を使用します。 最新のすべてのJavaScript環境で使用できる必要があります。



Lo-Dashの例は次のとおりです。



JS Binコード

コードを表示
var a = [1, 2, 3]; var b = [1, 2, 3]; var c = _.map(a, function(i) { return i * 2; }); console.assert(a !== b); console.assert(_.isEqual(a, b)); console.assert(_.isEqual([2, 4, 6], c));
      
      





コンソール:

本当
本当
本当 




オブジェクト-スコープ(スコープ)



角度スコープオブジェクトは、標準的な方法でプロパティを追加できる通常のJavaScriptオブジェクトです。 これらは、 Scopeコンストラクターを使用して作成されます。 最も簡単な実装を書きましょう。



 function Scope() { }
      
      





ここで、 new演算子を使用して、スコープオブジェクトを作成し、プロパティを追加できます。



 var aScope = new Scope(); aScope.firstName = 'Jane'; aScope.lastName = 'Smith';
      
      





これらのプロパティには特別なものはありません。 特別なセッターを割り当てる必要はありません;値の型に制限はありません。 代わりに、すべての魔法は2つの関数にあります: $ watch$ digestです。



オブジェクトプロパティの監視:$ watchおよび$ digest



$ watch$ digestは、同じコインの表裏です。 これらは一緒になって、Angularのスコープオブジェクトの中心であるデータ変更への反応を形成します。



$ watchを使用して、スコープに「オブザーバー」を追加できます。 オブザーバーは、適切なスコープで変更が発生したときに通知されるものです。



オブザーバーは、2つの関数を$ watchに渡すことで作成されます。





Angularでは、監視関数の代わりに監視式を使用しました。 これは、バインド時にhtmlで、ディレクティブの属性として、またはJavaScriptから直接指定した文字列(「user.firstName」のようなもの)です。 この行は、Angularによって解析されてコンパイルされ、私たちのものと同様の監視関数になりました。 これがどのように行われるかについては、次の記事で説明します。 同じ記事では、監視機能を使用した低レベルのアプローチに従います。


$ watchを実装するには 、登録されたすべてのオブザーバーをどこかに保存する必要があります。 それらの配列をScopeコンストラクターに追加しましょう。



 function Scope() { this.$$watchers = []; }
      
      





$$プレフィックスは、変数がAngularフレームワークでプライベートであり、アプリケーションコードから呼び出されないことを意味します。



これで、関数$ watchを定義できます。 2つの関数を引数として受け取り、 $$ウォッチャー配列に格納します。 すべてのスコープオブジェクトがこの関数を必要とすることが想定されているので、プロトタイプに入れましょう。



 Scope.prototype.$watch = function(watchFn, listenerFn) { var watcher = { watchFn: watchFn, listenerFn: listenerFn }; this.$$watchers.push(watcher); };
      
      





コインの裏側は$ダイジェスト機能です。 特定のスコープに登録されているすべてのオブザーバーを起動します。 すべてのオブザーバーが単純にソートされ、それぞれがリスナー関数を呼び出す最も単純な実装を説明しましょう。



 Scope.prototype.$digest = function() { _.forEach(this.$$watchers, function(watch) { watch.listenerFn(); }); };
      
      





これで、オブザーバーを登録し、 $ digestを実行して、その結果としてリスナー関数が機能するようになります。



JS Binコード

コードを表示
 function Scope() { this.$$watchers = []; } Scope.prototype.$watch = function(watchFn, listenerFn) { var watcher = { watchFn: watchFn, listenerFn: listenerFn }; this.$$watchers.push(watcher); }; Scope.prototype.$digest = function() { _.forEach(this.$$watchers, function(watch) { watch.listenerFn(); }); }; var scope = new Scope(); scope.$watch( function() {console.log('watchFn'); }, function() {console.log('listener'); } ); scope.$digest(); scope.$digest(); scope.$digest();
      
      





コンソール:

 「リスナー」
 「リスナー」
 「リスナー」 




これ自体は特に有用ではありません。 ウォッチ関数で示されたデータが実際に変更された場合にのみ、ハンドラーを起動することが本当に望まれます。



データ変更検出



前述のように、オブザーバーの監視機能は、変更が関心のあるデータを返す必要があります。 通常、このデータはスコープ内にあるため、便宜上、スコープは引数として渡されます。 スコープからfirstNameを監視する監視関数は、次のようになります。



 function(scope) { return scope.firstName; }
      
      





ほとんどの場合、監視関数は次のようになります。対象のデータをスコープから抽出して返します。



$ダイジェストの機能は、この監視関数を呼び出し、それから受け取った値と前回返された値を比較することです。 値が異なる場合、データは「ダーティ」であり、対応するリスナー関数を呼び出す必要があります。



これを行うには、 $ダイジェストは各監視関数の最後に返された値を記憶する必要があります。また、各オブザーバーに独自のオブジェクトが既にあるため、このデータを格納するのが最も便利です。 以下に、 $ダイジェスト関数の新しい実装を示します。これは、各オブザーバーの変更についてデータをチェックします。



 Scope.prototype.$digest = function() { var self = this; _.forEach(this.$$watchers, function(watch) { var newValue = watch.watchFn(self); var oldValue = watch.last; if (newValue !== oldValue) { watch.listenerFn(newValue, oldValue, self); } watch.last = newValue; }); };
      
      





監視関数は各オブザーバーに対して呼び出され、現在のスコープを引数として渡します。 さらに、取得した値は、 最後の属性に格納されている以前の値と比較されます。 値が異なる場合、リスナーが呼び出されます。 便宜上、値とスコープの両方が引数としてリスナーに渡されます。 最後に、オブザーバの最後の属性に新しい値が書き込まれるため、次回の比較が可能になります。



$ダイジェストが呼び出されたときに、この実装でリスナーがどのようにトリガーされるかを見てみましょう。



JS Binコード

コードを表示
 function Scope() { this.$$watchers = []; } Scope.prototype.$watch = function(watchFn, listenerFn) { var watcher = { watchFn: watchFn, listenerFn: listenerFn }; this.$$watchers.push(watcher); }; Scope.prototype.$digest = function() { var self = this; _.forEach(this.$$watchers, function(watch) { var newValue = watch.watchFn(self); var oldValue = watch.last; if (newValue !== oldValue) { watch.listenerFn(newValue, oldValue, self); } watch.last = newValue; }); }; var scope = new Scope(); scope.firstName = 'Joe'; scope.counter = 0; scope.$watch( function(scope) { return scope.firstName; }, function(newValue, oldValue, scope) { scope.counter++; } ); // We haven't run $digest yet so counter should be untouched: console.assert(scope.counter === 0); // The first digest causes the listener to be run scope.$digest(); console.assert(scope.counter === 1); // Further digests don't call the listener... scope.$digest(); scope.$digest(); console.assert(scope.counter === 1); // ... until the value that the watch function is watching changes again scope.firstName = 'Jane'; scope.$digest(); console.assert(scope.counter === 2);
      
      





コンソール:

本当
本当
本当
本当 




これで、Angularスコープのコアを既に実装しました。オブザーバーを登録し、 $ダイジェスト関数でそれらを起動します。



これで、Angularのスコープのパフォーマンスに関するいくつかの結論を導き出すことができます。





ダイジェストアラート



$ダイジェストが実行されているというアラートを受信する必要がある場合、 ダイジェストを処理する過程で各監視機能が確実に起動するという事実を利用できます。 リスナーなしで監視機能を登録するだけです。



これを考慮するには、リスナーがスキップされていないかどうかを$ watch関数でチェックインする必要があります。チェックインされている場合は、代わりにスタブ関数を置き換えます。



 Scope.prototype.$watch = function(watchFn, listenerFn) { var watcher = { watchFn: watchFn, listenerFn: listenerFn || function() { } }; this.$$watchers.push(watcher); };
      
      





このテンプレートを使用する場合、Angularは、リスナー関数が宣言されていない場合でも、監視関数から返される値を考慮することに注意してください。 値を返すと、変更のチェックに参加します。 不要な作業を引き起こさないために-関数から何も返さないでください。undefinedは常にデフォルトで返されます:



JS Binコード

コードを表示
 function Scope() { this.$$watchers = []; } Scope.prototype.$watch = function(watchFn, listenerFn) { var watcher = { watchFn: watchFn, listenerFn: listenerFn || function() { } }; this.$$watchers.push(watcher); }; Scope.prototype.$digest = function() { var self = this; _.forEach(this.$$watchers, function(watch) { var newValue = watch.watchFn(self); var oldValue = watch.last; if (newValue !== oldValue) { watch.listenerFn(newValue, oldValue, self); } watch.last = newValue; }); }; var scope = new Scope(); scope.$watch(function() { console.log('digest listener fired'); }); scope.$digest(); scope.$digest(); scope.$digest();
      
      





コンソール:

 「ダイジェストリスナー解雇」
 「ダイジェストリスナー解雇」
 「ダイジェストリスナー解雇」 




カーネルの準備はできていますが、まだ完全ではありません。 たとえば、かなり典型的なシナリオは考慮されていません。リスナー関数自体がスコープからプロパティを変更できます。 これが発生し、別のオブザーバーがこのプロパティを監視した場合、少なくともこのパッセージ$ digestで、このオブザーバーが変更の通知を受け取らないことが判明する場合があります。



JS Binコード

コードを表示
 function Scope() { this.$$watchers = []; } Scope.prototype.$watch = function(watchFn, listenerFn) { var watcher = { watchFn: watchFn, listenerFn: listenerFn || function() {} }; this.$$watchers.push(watcher); }; Scope.prototype.$digest = function() { var self = this; _.forEach(this.$$watchers, function(watch) { var newValue = watch.watchFn(self); var oldValue = watch.last; if (newValue !== oldValue) { watch.listenerFn(newValue, oldValue, self); } watch.last = newValue; }); }; var scope = new Scope(); scope.firstName = 'Joe'; scope.counter = 0; scope.$watch( function(scope) { return scope.counter; }, function(newValue, oldValue, scope) { scope.counterIsTwo = (newValue === 2); } ); scope.$watch( function(scope) { return scope.firstName; }, function(newValue, oldValue, scope) { scope.counter++; } ); // After the first digest the counter is 1 scope.$digest(); console.assert(scope.counter === 1); // On the next change the counter becomes two, but our other watch hasn't noticed this yet scope.firstName = 'Jane'; scope.$digest(); console.assert(scope.counter === 2); console.assert(scope.counterIsTwo); // false // Only sometime in the future, when $digest() is called again, does our other watch get run scope.$digest(); console.assert(scope.counterIsTwo); // true
      
      





コンソール:

本当
本当
偽
本当 




それを修正しましょう。



ダーティデータがある限り、$ダイジェストを実行します



監視された値が変化しなくなるまでチェックを続けるように、 $ダイジェストを修正する必要があります。



最初に、現在の$ダイジェスト関数の名前を$$ digestOnceに変更し、すべての監視関数を1回実行して、監視フィールドの値に少なくとも1つの変更があったかどうかを示すブール変数を返すように変更します。



 Scope.prototype.$$digestOnce = function() { var self = this; var dirty; _.forEach(this.$$watchers, function(watch) { var newValue = watch.watchFn(self); var oldValue = watch.last; if (newValue !== oldValue) { watch.listenerFn(newValue, oldValue, self); dirty = true; } watch.last = newValue; }); return dirty; };
      
      





その後、 $ダイジェスト関数を再宣言して、変更がある限りループ内で$$ digestOnceが開始されるようにします。



 Scope.prototype.$digest = function() { var dirty; do { dirty = this.$$digestOnce(); } while (dirty); };
      
      





$ダイジェストは、登録された監視機能を少なくとも1回実行するようになりました。 最初のパスで観測値のいずれかが変更された場合、パスは「ダーティ」としてマークされ、2番目のパスが開始されます。 これは、単一の変更された値がパッセージ全体で検出されるまで起こります-状況は安定します。

Angularのスコープには、実際には$$ digestOnce関数はありません。 代わりに、この機能は$ダイジェストで直接ループに組み込まれます。 私たちの目的では、パフォーマンスよりも明快さと読みやすさが重要です。そのため、リファクタリングを少し行いました。


動作中の新しい実装は次のとおりです。



JS Binコード

コードを表示
 function Scope() { this.$$watchers = []; } Scope.prototype.$watch = function(watchFn, listenerFn) { var watcher = { watchFn: watchFn, listenerFn: listenerFn ||function() { } }; this.$$watchers.push(watcher); }; Scope.prototype.$$digestOnce = function() { var self = this; var dirty; _.forEach(this.$$watchers, function(watch) { var newValue = watch.watchFn(self); var oldValue = watch.last; if (newValue !== oldValue) { watch.listenerFn(newValue, oldValue, self); dirty = true; } watch.last = newValue; }); return dirty; }; Scope.prototype.$digest = function() { var dirty; do { dirty = this.$$digestOnce(); } while (dirty); }; var scope = new Scope(); scope.firstName = 'Joe'; scope.counter = 0; scope.$watch( function(scope) { return scope.counter; }, function(newValue, oldValue, scope) { scope.counterIsTwo = (newValue === 2); } ); scope.$watch( function(scope) { return scope.firstName; }, function(newValue, oldValue, scope) { scope.counter++; } ); // After the first digest the counter is 1 scope.$digest(); console.assert(scope.counter === 1); // On the next change the counter becomes two, and the other watch listener is also run because of the dirty check scope.firstName = 'Jane'; scope.$digest(); console.assert(scope.counter === 2); console.assert(scope.counterIsTwo);
      
      





コンソール:

本当
本当
本当 




ウォッチ機能に関してもう1つの重要な結論を出すことができます。これらは、$ダイジェスト操作中に数回動作することがあります。 そのため、時計機能はべき等であるとよく言われます。機能に副作用がないか、何回か正常に機能するような副作用があるはずです。 たとえば、監視機能にAJAXリクエストがある場合、このリクエストが何回実行されるかについての保証はありません。



現在の実装には1つの大きな欠陥があります。2人のオブザーバーが互いの変更を監視するとどうなりますか? この場合、状況は決して安定しませんか? 同様の状況が以下のコードに実装されています。 この例では、 $ダイジェストの呼び出しコメント化されています。



コメントを外して、何が起こるかを調べます。



JS Binコード

コードを表示
 function Scope() { this.$$watchers = []; } Scope.prototype.$watch = function(watchFn, listenerFn) { var watcher = { watchFn: watchFn, listenerFn: listenerFn || function() { } }; this.$$watchers.push(watcher); }; Scope.prototype.$$digestOnce = function() { var self = this; var dirty; _.forEach(this.$$watchers, function(watch) { var newValue = watch.watchFn(self); var oldValue = watch.last; if (newValue !== oldValue) { watch.listenerFn(newValue, oldValue, self); dirty = true; } watch.last = newValue; }); return dirty; }; Scope.prototype.$digest = function() { var dirty; do { dirty = this.$$digestOnce(); } while (dirty); }; var scope = new Scope(); scope.counter1 = 0; scope.counter2 = 0; scope.$watch( function(scope) { return scope.counter1; }, function(newValue, oldValue, scope) { scope.counter2++; } ); scope.$watch( function(scope) { return scope.counter2; }, function(newValue, oldValue, scope) { scope.counter1++; } ); // Uncomment this to run the digest // scope.$digest(); console.log(scope.counter1);
      
      





コンソール:

 0 




JSBinはしばらくすると機能を停止します(私のマシンでは約100,000回の反復が行われます)。 たとえば、node.jsの下でこのコードを実行すると、永久に実行されます。



$ダイジェストの不安定性を取り除く



必要なのは、 $ダイジェストを特定の反復回数に制限することだけです。 繰り返しが終了してもスコープが変化し続ける場合は、手を挙げてあきらめます-状態は決して安定しません。 この状況では、スコープの状態は明らかにユーザーが期待したものではないため、例外がスローされる可能性があります。



反復の最大数はTTL(存続時間-ライフタイムの略)と呼ばれます。 デフォルトでは10に設定します。この数字は小さいように見えます(約100,000回ダイジェストを実行しました)が、これはすでにパフォーマンスの問題であることに注意してください。 さらに、ユーザーが10個を超えるウォッチ機能をチェーンに配置することはほとんどありません。

角度TTLはカスタマイズ可能です。 これについては、プロバイダーと依存性注入について説明する今後の記事で説明します。


続けましょう-ダイジェストループにカウンターを追加しましょう。 TTLに達した場合-例外をスローします:



 Scope.prototype.$digest = function() { var ttl = 10; var dirty; do { dirty = this.$$digestOnce(); if (dirty && !(ttl--)) { throw "10 digest iterations reached"; } } while (dirty); };
      
      





前の例の更新バージョンは、例外をスローします。



JS Binコード

コードを表示
 function Scope() { this.$$watchers = []; } Scope.prototype.$watch = function(watchFn, listenerFn) { var watcher = { watchFn: watchFn, listenerFn: listenerFn || function() { } }; this.$$watchers.push(watcher); }; Scope.prototype.$$digestOnce = function() { var self = this; var dirty; _.forEach(this.$$watchers, function(watch) { var newValue = watch.watchFn(self); var oldValue = watch.last; if (newValue !== oldValue) { watch.listenerFn(newValue, oldValue, self); dirty = true; } watch.last = newValue; }); return dirty; }; Scope.prototype.$digest = function(){ var ttl = 10; var dirty; do { dirty = this.$$digestOnce(); if (dirty && !(ttl--)) { throw "10 digest iterations reached"; } } while (dirty); }; var scope = new Scope(); scope.counter1 = 0; scope.counter2 = 0; scope.$watch( function(scope) { return scope.counter1; }, function(newValue, oldValue, scope) { scope.counter2++; } ); scope.$watch( function(scope) { return scope.counter2; }, function(newValue, oldValue, scope) { scope.counter1++; } ); scope.$digest();
      
      





コンソール:

 「10回のダイジェストの繰り返しに到達しなかった(36行目)」 




ダイジェストは固定を取り除きました。



ここで、何かが変更されたことを正確に判断する方法を見てみましょう。



値による変更を確認する



現時点では、厳密な等価演算子===を使用して新しい値と古い値を比較します。 ほとんどの場合、これは機能します。プリミティブ型(数値、文字列など)の変更は通常検出され、オブジェクトまたは配列が別のものに置き換えられるかどうかも決定されます。 ただし、Angularには変更を検出する別の方法があり、配列またはオブジェクト内で何かが変更されたかどうかを確認できます。 これを行うには、参照ではなくで比較する必要があります



このタイプの検証は、ブール型のオプションの3番目のパラメーターを$ watch関数に渡すことでアクティブにできます。 このフラグがtrueの場合、値チェックが使用されます。 $ watchを変更しましょう-フラグを取得して、オブザーバー( watcher変数)に保存します。



 Scope.prototype.$watch = function(watchFn, listenerFn, valueEq) { var watcher = { watchFn: watchFn, listenerFn: listenerFn, valueEq: !!valueEq }; this.$$watchers.push(watcher); };
      
      





オブザーバーにフラグを追加し、二重否定を使用して強制的にブール型にキャストしました。 ユーザーが3番目のパラメーターなしで$ watchを呼び出すと、 valueEqundefinedになり、ウォッチャーオブジェクトでfalseに変換されます。



値によるチェックは、値がオブジェクトまたは配列である場合、古いコンテンツと新しいコンテンツの両方を調べる必要があることを意味します。 違いがある場合、オブザーバーは「ダーティ」としてマークされます。 ネストされたオブジェクトまたは配列がコンテンツ内で発生する可能性があります。その場合、値によって再帰的にチェックする必要もあります。



Angularには独自の値比較関数がありますが、Lo-Dashの 関数を使用します。 値のペアとフラグを取る比較関数を書きましょう:



 Scope.prototype.$$areEqual = function(newValue, oldValue, valueEq) { if (valueEq) { return _.isEqual(newValue, oldValue); } else { return newValue === oldValue; } };
      
      





「値による」変化を判断するために、ガールフレンドが「古い値」を保存することも必要です。 現在の値へのリンクを保存するだけでは十分ではありません。変更が加えられると、参照によって保存されるオブジェクトにも反映されます。 同じデータへの2つのリンクが常に$$ areEqual関数に該当する場合、何かが変更されたかどうかを判断できません。 したがって、コンテンツの詳細コピーを作成し、このコピーを保存する必要があります。



比較関数の場合と同様に、Angularにはデータをディープコピーするための独自の関数がありますが、 Lo-Dashの同じ 関数を使用します。 比較のために$$ areEqualを使用し、必要に応じて最後にコピーを作成するように$$ digestOnceを改良しましょう。



 Scope.prototype.$$digestOnce = function() { var self = this; var dirty; _.forEach(this.$$watchers, function(watch) { var newValue = watch.watchFn(self); var oldValue = watch.last; if (!self.$$areEqual(newValue, oldValue, watch.valueEq)) { watch.listenerFn(newValue, oldValue, self); dirty = true; } watch.last = (watch.valueEq ? _.cloneDeep(newValue) : newValue); }); return dirty; };
      
      





これで、値を比較する2つの方法の違いを確認できます。



JS Binコード

コードを表示
 function Scope() { this.$$watchers = []; } Scope.prototype.$watch = function(watchFn, listenerFn, valueEq) { var watcher = { watchFn: watchFn, listenerFn: listenerFn || function() { }, valueEq: !!valueEq }; this.$$watchers.push(watcher); }; Scope.prototype.$$areEqual = function(newValue, oldValue, valueEq) { if (valueEq) { return _.isEqual(newValue, oldValue); } else { return newValue === oldValue; } }; Scope.prototype.$$digestOnce = function() { var self = this; var dirty; _.forEach(this.$$watchers, function(watch) { var newValue = watch.watchFn(self); var oldValue = watch.last; if (!self.$$areEqual(newValue, oldValue, watch.valueEq)) { watch.listenerFn(newValue, oldValue, self); dirty = true; } watch.last = (watch.valueEq ? _.cloneDeep(newValue) : newValue); }); return dirty; }; Scope.prototype.$digest = function(){ var ttl = 10; var dirty; do { dirty = this.$$digestOnce(); if (dirty && !(ttl--)) { throw "10 digest iterations reached"; } } while (dirty); }; var scope = new Scope(); scope.counterByRef = 0; scope.counterByValue = 0; scope.value = [1, 2, {three: [4, 5]}]; // Set up two watches for value. One checks references, the other by value. scope.$watch( function(scope) { return scope.value; }, function(newValue, oldValue, scope) { scope.counterByRef++; } ); scope.$watch( function(scope) { return scope.value; }, function(newValue, oldValue, scope) { scope.counterByValue++; }, true ); scope.$digest(); console.assert(scope.counterByRef === 1); console.assert(scope.counterByValue === 1); // When changes are made within the value, the by-reference watcher does not notice, but the by-value watcher does. scope.value[2].three.push(6); scope.$digest(); console.assert(scope.counterByRef === 1); console.assert(scope.counterByValue === 2); // Both watches notice when the reference changes. scope.value = {aNew: "value"}; scope.$digest(); console.assert(scope.counterByRef === 2); console.assert(scope.counterByValue === 3); delete scope.value; scope.$digest(); console.assert(scope.counterByRef === 3); console.assert(scope.counterByValue === 4);
      
      





コンソール:

本当
本当
本当
本当
本当
本当 




値による検証は、参照による検証よりも明らかにリソースに厳しいです。ネストされた構造の繰り返しには時間がかかり、オブジェクトのコピーを保存するとメモリの消費量が増えます。これが、Angularがデフォルトでデフォルトの検証を使用しない理由です。フラグを明示的に設定する必要があります。

また、Angularには、値の変更をチェックするための3番目のメカニズム「コレクションの監視」があります。値によるチェックのメカニズムと同様に、オブジェクトと配列の変更を明らかにしますが、それとは異なり、チェックはネストされたレベルに深く入ることなく簡単です。自然に高速です。コレクションの監視は、$ watchCollection関数を使用して利用できます。その実装については、シリーズの次の記事で説明します。


値の比較を完了する前に、JavaScriptの1つの機能を検討する必要があります。



NaN値



JavaScriptでは、値NaN(数値ではない)はそれ自体と等しくありません。おそらくそうであるため、これは奇妙に聞こえるかもしれません。変更の値をチェックする関数でNaNを手動で処理しなかったため、NaNを監視する監視関数は常にオブザーバーを「ダーティ」としてマークします。



「値による」テストでは、このケースはLo-DashのisEqual関数で既に考慮されています「参照による」チェックでは、自分で行う必要があります。それでは、$$ areEqual関数を改良しましょう



 Scope.prototype.$$areEqual = function(newValue, oldValue, valueEq) { if (valueEq) { return _.isEqual(newValue, oldValue); } else { return newValue === oldValue || (typeof newValue === 'number' && typeof oldValue === 'number' && isNaN(newValue) && isNaN(oldValue)); } };
      
      





NaNを持つオブザーバーは期待どおりに動作するようになりました:



JS Binコード

コードを表示
 function Scope() { this.$$watchers = []; } Scope.prototype.$watch = function(watchFn, listenerFn, valueEq) { var watcher = { watchFn: watchFn, listenerFn: listenerFn || function() {}, valueEq: !!valueEq }; this.$$watchers.push(watcher); }; Scope.prototype.$$areEqual = function(newValue, oldValue, valueEq) { if (valueEq) { return _.isEqual(newValue, oldValue); } else { return newValue === oldValue || (typeof newValue === 'number' && typeof oldValue === 'number' && isNaN(newValue) && isNaN(oldValue)); } }; Scope.prototype.$$digestOnce = function() { var self = this; var dirty; _.forEach(this.$$watchers, function(watch) { var newValue = watch.watchFn(self); var oldValue = watch.last; if (!self.$$areEqual(newValue, oldValue, watch.valueEq)) { watch.listenerFn(newValue, oldValue, self); dirty = true; } watch.last = (watch.valueEq ? _.cloneDeep(newValue) : newValue); }); return dirty; }; Scope.prototype.$digest = function(){ var ttl = 10; var dirty; do { dirty = this.$$digestOnce(); if (dirty && !(ttl--)) { throw "10 digest iterations reached"; } } while (dirty); }; var scope = new Scope(); scope.number = 0; scope.counter = 0; scope.$watch( function(scope) { return scope.number; }, function(newValue, oldValue, scope) { scope.counter++; } ); scope.$digest(); console.assert(scope.counter === 1); scope.number = parseInt('wat', 10); // Becomes NaN scope.$digest(); console.assert(scope.counter === 2);
      
      





コンソール:

本当
本当 




ここで、値をチェックすることから、アプリケーションコードからスコープを操作する方法に焦点を移しましょう。



$ eval-スコープコンテキストでのコード実行



Angularには、スコープコンテキストでコードを実行するためのオプションがいくつかあります。これらの中で最も単純なのは、$ eval関数です。これは関数を引数として取り、それが行うことはただちにそれを呼び出すことであり、現在のスコープをパラメーターとして渡します。それでは、実行結果を返します。$ evalは2番目のパラメーターも受け取ります。このパラメーターは、呼び出された関数にそのまま渡されます。$ eval



の実装非常に簡単です:



 Scope.prototype.$eval = function(expr, locals) { return expr(this, locals); };
      
      







$ evalの使用も非常に簡単です:



JS Binコード

コードを表示
 function Scope() { this.$$watchers = []; } Scope.prototype.$watch = function(watchFn, listenerFn, valueEq) { var watcher = { watchFn: watchFn, listenerFn: listenerFn || function() {}, valueEq: !!valueEq }; this.$$watchers.push(watcher); }; Scope.prototype.$$areEqual = function(newValue, oldValue, valueEq) { if (valueEq) { return _.isEqual(newValue, oldValue); } else { return newValue === oldValue || (typeof newValue === 'number' && typeof oldValue === 'number' && isNaN(newValue) && isNaN(oldValue)); } }; Scope.prototype.$$digestOnce = function() { var self = this; var dirty; _.forEach(this.$$watchers, function(watch) { var newValue = watch.watchFn(self); var oldValue = watch.last; if (!self.$$areEqual(newValue, oldValue, watch.valueEq)) { watch.listenerFn(newValue, oldValue, self); dirty = true; } watch.last = (watch.valueEq ? _.cloneDeep(newValue) : newValue); }); return dirty; }; Scope.prototype.$digest = function(){ var ttl = 10; var dirty; do { dirty = this.$$digestOnce(); if (dirty && !(ttl--)) { throw "10 digest iterations reached"; } } while (dirty); }; Scope.prototype.$eval = function(expr, locals) { return expr(this, locals); }; var scope = new Scope(); scope.number = 1; scope.$eval(function(theScope) { console.log('Number during $eval:', theScope.number); });
      
      





コンソール:

「$評価中の数:」
 1 




? , $eval , scope. $eval $apply , .



, $eval “” . $watch , $eval . scope. , .



$apply — $digest



おそらく$ applyは、すべてのScope関数の中で最も有名です。これは、サードパーティのライブラリをAngularと統合する標準的な方法として位置付けられています。そして、これには理由があります。



$ applyは関数を引数として受け入れ、$ evalを使用してこの関数を呼び出しますが、最終的には$ digestを開始します。最も簡単な実装を次に示します。



 Scope.prototype.$apply = function(expr) { try { return this.$eval(expr); } finally { this.$digest(); } };
      
      





関数で例外が発生した場合でも、依存関係を更新するために、finallyブロック$ダイジェストが呼び出されます。$ apply



を使用すると、Angularに馴染みのないコードを実行できます。このコードはスコープ内のデータを変更でき、$ applyはオブザーバーがこれらの変更を確実にキャッチするようにします。これらは、「Angularライフサイクルへのコードの統合」について話すときの意味です。これはこれ以上のものではありません。



$実行中に適用



JS Binコード

コードを表示
 function Scope() { this.$$watchers = []; } Scope.prototype.$watch = function(watchFn, listenerFn, valueEq) { var watcher = { watchFn: watchFn, listenerFn: listenerFn || function() {}, valueEq: !!valueEq }; this.$$watchers.push(watcher); }; Scope.prototype.$$areEqual = function(newValue, oldValue, valueEq) { if (valueEq) { return _.isEqual(newValue, oldValue); } else { return newValue === oldValue || (typeof newValue === 'number' && typeof oldValue === 'number' && isNaN(newValue) && isNaN(oldValue)); } }; Scope.prototype.$$digestOnce = function() { var self = this; var dirty; _.forEach(this.$$watchers, function(watch) { var newValue = watch.watchFn(self); var oldValue = watch.last; if (!self.$$areEqual(newValue, oldValue, watch.valueEq)) { watch.listenerFn(newValue, oldValue, self); dirty = true; } watch.last = (watch.valueEq ? _.cloneDeep(newValue) : newValue); }); return dirty; }; Scope.prototype.$digest = function(){ var ttl = 10; var dirty; do { dirty = this.$$digestOnce(); if (dirty && !(ttl--)) { throw "10 digest iterations reached"; } } while (dirty); }; Scope.prototype.$eval = function(expr, locals) { return expr(this, locals); }; Scope.prototype.$apply = function(expr) { try { return this.$eval(expr); } finally { this.$digest(); } }; var scope = new Scope(); scope.counter = 0; scope.$watch( function(scope) { return scope.aValue; }, function(newValue, oldValue, scope) { scope.counter++; } ); scope.$apply(function(scope) { scope.aValue = 'Hello from "outside"'; }); console.assert(scope.counter === 1);
      
      





コンソール:

本当 






遅延実行-$ evalAsync



JavaScriptでは、多くの場合、「後で」コードを実行する必要があります。つまり、現在の実行コンテキスト内のすべてのコードが実行されるまで実行を遅らせます。これは通常、ゼロ(またはゼロに近い)遅延でSetTimeout()使用して行われます。



このトリックはAngularアプリケーションでも機能しますが、これには$タイムアウトサービス を使用することをお勧めします。これは、特に$ applyを使用して、遅延関数の呼び出しをダイジェストループに統合しますしかし、Angularでコードの実行を延期する別の方法があります- $ evalAsync関数



関数をパラメーターとして受け取り、後で確実に実行されますが、現在のダイジェストサイクル内で直接実行される場合(現在実行中の場合)、または次のダイジェストサイクルの直前に実行されます。たとえば、オブザーバーのリスナー関数からコードの実行を直接延期することができます。コードが遅延しているにもかかわらず、ダイジェストループの次の反復で実行されることを知っています。



まず、$$ evalAsyncを介して延期されたタスクを保存する場所を決定する必要があります。Scopeコンストラクターで初期化することで、これに配列を使用できます



 function Scope() { this.$$watchers = []; this.$$asyncQueue = []; }
      
      





次に、キューに関数を追加する$ evalAsync自体を記述します。



 Scope.prototype.$evalAsync = function(expr) { this.$$asyncQueue.push({scope: this, expression: expr}); };
      
      





キューオブジェクトにスコープを明示的に追加する理由は、スコープの継承によるものです。これについては、このシリーズの次回の記事で説明します。


ここで、$ダイジェストで最初に行うことは、遅延開始キューにあるすべての関数を抽出し、$ evalを使用してそれらを実行することです。



 Scope.prototype.$digest = function() { var ttl = 10; var dirty; do { while (this.$$asyncQueue.length) { var asyncTask = this.$$asyncQueue.shift(); this.$eval(asyncTask.expression); } dirty = this.$$digestOnce(); if (dirty && !(ttl--)) { throw "10 digest iterations reached"; } } while (dirty); };
      
      





この実装により、関数の実行を遅延させ、スコープがダーティとしてマークされた場合、その関数は同じダイジェストループで遅延と呼ばれます。$ evalAsyncの使用



方法の例を次に示します。JS



Binコード

コードを表示
 function Scope() { this.$$watchers = []; this.$$asyncQueue = []; } Scope.prototype.$watch = function(watchFn, listenerFn, valueEq) { var watcher = { watchFn: watchFn, listenerFn: listenerFn || function() {}, valueEq: !!valueEq }; this.$$watchers.push(watcher); }; Scope.prototype.$$areEqual = function(newValue, oldValue, valueEq) { if (valueEq) { return _.isEqual(newValue, oldValue); } else { return newValue === oldValue || (typeof newValue === 'number' && typeof oldValue === 'number' && isNaN(newValue) && isNaN(oldValue)); } }; Scope.prototype.$$digestOnce = function() { var self = this; var dirty; _.forEach(this.$$watchers, function(watch) { var newValue = watch.watchFn(self); var oldValue = watch.last; if (!self.$$areEqual(newValue, oldValue, watch.valueEq)) { watch.listenerFn(newValue, oldValue, self); dirty = true; } watch.last = (watch.valueEq ? _.cloneDeep(newValue) : newValue); }); return dirty; }; Scope.prototype.$digest = function() { var ttl = 10; var dirty; do { while (this.$$asyncQueue.length) { var asyncTask = this.$$asyncQueue.shift(); this.$eval(asyncTask.expression); } dirty = this.$$digestOnce(); if (dirty && !(ttl--)) { throw "10 digest iterations reached"; } } while (dirty); }; Scope.prototype.$eval = function(expr, locals) { return expr(this, locals); }; Scope.prototype.$apply = function(expr) { try { return this.$eval(expr); } finally { this.$digest(); } }; Scope.prototype.$evalAsync = function(expr) { this.$$asyncQueue.push({scope: this, expression: expr}); }; var scope = new Scope(); scope.asyncEvaled = false; scope.$watch( function(scope) { return scope.aValue; }, function(newValue, oldValue, scope) { scope.counter++; scope.$evalAsync(function(scope) { scope.asyncEvaled = true; }); console.log("Evaled inside listener: "+scope.asyncEvaled); } ); scope.aValue = "test"; scope.$digest(); console.log("Evaled after digest: "+scope.asyncEvaled);
      
      





コンソール:

「リスナー内で評価:false」
「ダイジェスト後に評価:true」 




スコープ内のフェーズ



$ evalAsync関数は別のことを行います。ダイジェストが実行されていない場合は、ダイジェストを実行するようにスケジュールする必要があります。この点は$ evalAsyncを呼び出すときはいつでも、他の何かがダイジェストを開始するときではなく、遅延関数が「かなりすぐに」実行されることを確認する必要があるということです。



$ evalAsyncは、ダイジェストが現在実行中かどうかを何らかの形で理解する必要があります。この目的のために、Angularスコープは「フェーズ」と呼ばれるメカニズムを実装します。これは、スコープ内の通常の文字列であり、現在何が起こっているかに関する情報を保存します。$$フェーズフィールドを$スコープ



コンストラクター追加しnullに設定します



 function Scope() { this.$$watchers = []; this.$$asyncQueue = []; this.$$phase = null; }
      
      





次に、フェーズを制御するいくつかの関数を作成してみましょう。1つはインストール用、もう1つはクリーニング用です。また、前のフェーズを完了せずにフェーズを確立しようとしていないことを確認する追加のチェックを追加します。



 Scope.prototype.$beginPhase = function(phase) { if (this.$$phase) { throw this.$$phase + ' already in progress.'; } this.$$phase = phase; }; Scope.prototype.$clearPhase = function() { this.$$phase = null; };
      
      





$ダイジェスト関数で、「$ダイジェスト」フェーズを設定し、ダイジェストループをラップします。



 Scope.prototype.$digest = function() { var ttl = 10; var dirty; this.$beginPhase("$digest"); do { while (this.$$asyncQueue.length) { var asyncTask = this.$$asyncQueue.shift(); this.$eval(asyncTask.expression); } dirty = this.$$digestOnce(); if (dirty && !(ttl--)) { this.$clearPhase(); throw "10 digest iterations reached"; } } while (dirty); this.$clearPhase(); };
      
      





ここにいる間に、$ applyを同時に確定して、フェーズをここに記述します。これは、デバッグプロセスで役立ちます。



 Scope.prototype.$apply = function(expr) { try { this.$beginPhase("$apply"); return this.$eval(expr); } finally { this.$clearPhase(); this.$digest(); } };
      
      





これで、最終的に$ evalAsync関数で$ digestの呼び出しをスケジュールできますここでは、フェーズが空の場合(および非同期タスクがまだ計画されていない場合)フェーズをチェックする必要があります- $ digestを実行する予定です:



 Scope.prototype.$evalAsync = function(expr) { var self = this; if (!self.$$phase && !self.$$asyncQueue.length) { setTimeout(function() { if (self.$$asyncQueue.length) { self.$digest(); } }, 0); } self.$$asyncQueue.push({scope: self, expression: expr}); };
      
      





この実装では、$ evalAsyncを呼び出すことにより、呼び出し元に関係なく、近い将来ダイジェストが発生することを確認できます。JSBin



コード

コードを表示
 function Scope() { this.$$watchers = []; this.$$asyncQueue = []; this.$$phase = null; } Scope.prototype.$beginPhase = function(phase) { if (this.$$phase) { throw this.$$phase + ' already in progress.'; } this.$$phase = phase; }; Scope.prototype.$clearPhase = function() { this.$$phase = null; }; Scope.prototype.$watch = function(watchFn, listenerFn, valueEq) { var watcher = { watchFn: watchFn, listenerFn: listenerFn || function() {}, valueEq: !!valueEq }; this.$$watchers.push(watcher); }; Scope.prototype.$$areEqual = function(newValue, oldValue, valueEq) { if (valueEq) { return _.isEqual(newValue, oldValue); } else { return newValue === oldValue || (typeof newValue === 'number' && typeof oldValue === 'number' && isNaN(newValue) && isNaN(oldValue)); } }; Scope.prototype.$$digestOnce = function() { var self = this; var dirty; _.forEach(this.$$watchers, function(watch) { var newValue = watch.watchFn(self); var oldValue = watch.last; if (!self.$$areEqual(newValue, oldValue, watch.valueEq)) { watch.listenerFn(newValue, oldValue, self); dirty = true; } watch.last = (watch.valueEq ? _.cloneDeep(newValue) : newValue); }); return dirty; }; Scope.prototype.$digest = function() { var ttl = 10; var dirty; this.$beginPhase("$digest"); do { while (this.$$asyncQueue.length) { var asyncTask = this.$$asyncQueue.shift(); this.$eval(asyncTask.expression); } dirty = this.$$digestOnce(); if (dirty && !(ttl--)) { this.$clearPhase(); throw "10 digest iterations reached"; } } while (dirty); this.$clearPhase(); }; Scope.prototype.$eval = function(expr, locals) { return expr(this, locals); }; Scope.prototype.$apply = function(expr) { try { this.$beginPhase("$apply"); return this.$eval(expr); } finally { this.$clearPhase(); this.$digest(); } }; Scope.prototype.$evalAsync = function(expr) { var self = this; if (!self.$$phase && !self.$$asyncQueue.length) { setTimeout(function() { if (self.$$asyncQueue.length) { self.$digest(); } }, 0); } self.$$asyncQueue.push({scope: self, expression: expr}); }; var scope = new Scope(); scope.asyncEvaled = false; scope.$evalAsync(function(scope) { scope.asyncEvaled = true; }); setTimeout(function() { console.log("Evaled after a while: "+scope.asyncEvaled); }, 100); // Check after a delay to make sure the digest has had a chance to run.
      
      





コンソール:

「しばらくして評価:true」 




ダイジェスト後のコードの実行-$$ postDigest



digest- — $$postDigest .



, Angular-, Angular-. , .



$evalAsync , $$postDigest - “”. , , digest . $$postDigest $digest、したがって、遅延関数の起動は、サードパーティのコードがダイジェストを開始するまで遅延する場合があります。名前が示すように、$$ postDigestはダイジェストの直後に遅延関数のみを開始するため、$$ postDigest渡されるコードのスコープを変更した場合は、$ digestまたは$ apply明示的に使用して変更をキャッチアップする必要があります



開始するには、もう1つのキューをScopeコンストラクター追加しましょう。今回は$$ postDigestに使用します。



 function Scope() { this.$$watchers = []; this.$$asyncQueue = []; this.$$postDigestQueue = []; this.$$phase = null; }
      
      





次に、$$ postDigest自体を実装します彼女がすることは、受け入れられた関数をキューに追加することだけです。



 Scope.prototype.$$postDigest = function(fn) { this.$$postDigestQueue.push(fn); };
      
      





最後に、$ digestの最後で、一度にすべての関数を呼び出してキューをクリアする必要があります。



 Scope.prototype.$digest = function() { var ttl = 10; var dirty; this.$beginPhase("$digest"); do { while (this.$$asyncQueue.length) { var asyncTask = this.$$asyncQueue.shift(); this.$eval(asyncTask.expression); } dirty = this.$$digestOnce(); if (dirty && !(ttl--)) { this.$clearPhase(); throw "10 digest iterations reached"; } } while (dirty); this.$clearPhase(); while (this.$$postDigestQueue.length) { this.$$postDigestQueue.shift()(); } };
      
      





ここでは、使用する方法の一例である$$ postDigestは



JSビンコードに

コードを表示
 function Scope() { this.$$watchers = []; this.$$asyncQueue = []; this.$$postDigestQueue = []; this.$$phase = null; } Scope.prototype.$beginPhase = function(phase) { if (this.$$phase) { throw this.$$phase + ' already in progress.'; } this.$$phase = phase; }; Scope.prototype.$clearPhase = function() { this.$$phase = null; }; Scope.prototype.$watch = function(watchFn, listenerFn, valueEq) { var watcher = { watchFn: watchFn, listenerFn: listenerFn || function() {}, valueEq: !!valueEq }; this.$$watchers.push(watcher); }; Scope.prototype.$$areEqual = function(newValue, oldValue, valueEq) { if (valueEq) { return _.isEqual(newValue, oldValue); } else { return newValue === oldValue || (typeof newValue === 'number' && typeof oldValue === 'number' && isNaN(newValue) && isNaN(oldValue)); } }; Scope.prototype.$$digestOnce = function() { var self = this; var dirty; _.forEach(this.$$watchers, function(watch) { var newValue = watch.watchFn(self); var oldValue = watch.last; if (!self.$$areEqual(newValue, oldValue, watch.valueEq)) { watch.listenerFn(newValue, oldValue, self); dirty = true; } watch.last = (watch.valueEq ? _.cloneDeep(newValue) : newValue); }); return dirty; }; Scope.prototype.$digest = function() { var ttl = 10; var dirty; this.$beginPhase("$digest"); do { while (this.$$asyncQueue.length) { var asyncTask = this.$$asyncQueue.shift(); this.$eval(asyncTask.expression); } dirty = this.$$digestOnce(); if (dirty && !(ttl--)) { this.$clearPhase(); throw "10 digest iterations reached"; } } while (dirty); this.$clearPhase(); while (this.$$postDigestQueue.length) { this.$$postDigestQueue.shift()(); } }; Scope.prototype.$eval = function(expr, locals) { return expr(this, locals); }; Scope.prototype.$apply = function(expr) { try { this.$beginPhase("$apply"); return this.$eval(expr); } finally { this.$clearPhase(); this.$digest(); } }; Scope.prototype.$evalAsync = function(expr) { var self = this; if (!self.$$phase && !self.$$asyncQueue.length) { setTimeout(function() { if (self.$$asyncQueue.length) { self.$digest(); } }, 0); } self.$$asyncQueue.push({scope: self, expression: expr}); }; Scope.prototype.$$postDigest = function(fn) { this.$$postDigestQueue.push(fn); }; var scope = new Scope(); var postDigestInvoked = false; scope.$$postDigest(function() { postDigestInvoked = true; }); console.assert(!postDigestInvoked); scope.$digest(); console.assert(postDigestInvoked);
      
      





コンソール:

本当
本当 




例外処理



現在の$スコープの実装は、Angularバージョンにますます近づいています。しかし、それでもまだ非常に壊れやすいです。これは、例外処理に十分な注意を払っていないためです。



Angularのスコープオブジェクトはかなりエラー耐性があります。監視関数$ evalAsyncまたは$$ postDigest例外が発生しても、ダイジェストサイクルは中断されません。現在の実装では、これらのエラーはダイジェストから外れます。try ... catch



でこれらすべての関数の呼び出しブロックの内側をラップすることで、これを簡単に修正できます

Angularでは、これらのエラーは特別なサービス$ exceptionHandlerに渡されます。まだありませんので、今のところはコンソールにエラーを出力します。


$ evalAsyncおよび$$ postDigestの例外処理は、$ digest関数で行われますどちらの場合も、例外はログに記録され、ダイジェストは通常​​どおり続行されます。



 Scope.prototype.$digest = function() { var ttl = 10; var dirty; this.$beginPhase("$digest"); do { while (this.$$asyncQueue.length) { try { var asyncTask = this.$$asyncQueue.shift(); this.$eval(asyncTask.expression); } catch (e) { (console.error || console.log)(e); } } dirty = this.$$digestOnce(); if (dirty && !(ttl--)) { this.$clearPhase(); throw "10 digest iterations reached"; } } while (dirty); this.$clearPhase(); while (this.$$postDigestQueue.length) { try { this.$$postDigestQueue.shift()(); } catch (e) { (console.error || console.log)(e); } } };
      
      





監視機能の例外処理は、$ digestOnceで行われます



 Scope.prototype.$$digestOnce = function() { var self = this; var dirty; _.forEach(this.$$watchers, function(watch) { try { var newValue = watch.watchFn(self); var oldValue = watch.last; if (!self.$$areEqual(newValue, oldValue, watch.valueEq)) { watch.listenerFn(newValue, oldValue, self); dirty = true; } watch.last = (watch.valueEq ? _.cloneDeep(newValue) : newValue); } catch (e) { (console.error || console.log)(e); } }); return dirty; };
      
      





ダイジェストサイクルは、例外を除いてはるかに信頼性が高くなりました:



JS Binコード

コードを表示
 function Scope() { this.$$watchers = []; this.$$asyncQueue = []; this.$$postDigestQueue = []; this.$$phase = null; } Scope.prototype.$beginPhase = function(phase) { if (this.$$phase) { throw this.$$phase + ' already in progress.'; } this.$$phase = phase; }; Scope.prototype.$clearPhase = function() { this.$$phase = null; }; Scope.prototype.$watch = function(watchFn, listenerFn, valueEq) { var watcher = { watchFn: watchFn, listenerFn: listenerFn || function() {}, valueEq: !!valueEq }; this.$$watchers.push(watcher); }; Scope.prototype.$$areEqual = function(newValue, oldValue, valueEq) { if (valueEq) { return _.isEqual(newValue, oldValue); } else { return newValue === oldValue || (typeof newValue === 'number' && typeof oldValue === 'number' && isNaN(newValue) && isNaN(oldValue)); } }; Scope.prototype.$$digestOnce = function() { var self = this; var dirty; _.forEach(this.$$watchers, function(watch) { try { var newValue = watch.watchFn(self); var oldValue = watch.last; if (!self.$$areEqual(newValue, oldValue, watch.valueEq)) { watch.listenerFn(newValue, oldValue, self); dirty = true; } watch.last = (watch.valueEq ? _.cloneDeep(newValue) : newValue); } catch (e) { (console.error || console.log)(e); } }); return dirty; }; Scope.prototype.$digest = function() { var ttl = 10; var dirty; this.$beginPhase("$digest"); do { while (this.$$asyncQueue.length) { try { var asyncTask = this.$$asyncQueue.shift(); this.$eval(asyncTask.expression); } catch (e) { (console.error || console.log)(e); } } dirty = this.$$digestOnce(); if (dirty && !(ttl--)) { this.$clearPhase(); throw "10 digest iterations reached"; } } while (dirty); this.$clearPhase(); while (this.$$postDigestQueue.length) { try { this.$$postDigestQueue.shift()(); } catch (e) { (console.error || console.log)(e); } } }; Scope.prototype.$eval = function(expr, locals) { return expr(this, locals); }; Scope.prototype.$apply = function(expr) { try { this.$beginPhase("$apply"); return this.$eval(expr); } finally { this.$clearPhase(); this.$digest(); } }; Scope.prototype.$evalAsync = function(expr) { var self = this; if (!self.$$phase && !self.$$asyncQueue.length) { setTimeout(function() { if (self.$$asyncQueue.length) { self.$digest(); } }, 0); } self.$$asyncQueue.push({scope: self, expression: expr}); }; Scope.prototype.$$postDigest = function(fn) { this.$$postDigestQueue.push(fn); }; var scope = new Scope(); scope.aValue = "abc"; scope.counter = 0; scope.$watch(function() { throw "Watch fail"; }); scope.$watch( function(scope) { scope.$evalAsync(function(scope) { throw "async fail"; }); return scope.aValue; }, function(newValue, oldValue, scope) { scope.counter++; } ); scope.$digest(); console.assert(scope.counter === 1);
      
      





コンソール:

「ウォッチ失敗」
「非同期失敗」
「ウォッチ失敗」
本当 




オブザーバーの無効化



オブザーバーを登録する場合、ほとんどの場合、スコープオブジェクトの存続期間中アクティブに保つ必要があり、明示的に削除する必要はありません。ただし、場合によっては、スコープは引き続き機能するはずですが、何らかのオブザーバーを削除する必要がある場合があります。Angular



$ watch関数は、実際に値を返します-その呼び出しが登録済みオブザーバーを削除する関数。これを実装するために必要なのは、$ watch$$ watchers配列から新しく作成されたオブザーバーを削除する関数返すことだけです:



 Scope.prototype.$watch = function(watchFn, listenerFn, valueEq) { var self = this; var watcher = { watchFn: watchFn, listenerFn: listenerFn, valueEq: !!valueEq }; self.$$watchers.push(watcher); return function() { var index = self.$$watchers.indexOf(watcher); if (index >= 0) { self.$$watchers.splice(index, 1); } }; };
      
      





これで、$ watchから返された関数を覚えて、後でオブザーバーを破棄する必要があるときに呼び出すことができます:



JS Bin code

コードを表示
 function Scope() { this.$$watchers = []; this.$$asyncQueue = []; this.$$postDigestQueue = []; this.$$phase = null; } Scope.prototype.$beginPhase = function(phase) { if (this.$$phase) { throw this.$$phase + ' already in progress.'; } this.$$phase = phase; }; Scope.prototype.$clearPhase = function() { this.$$phase = null; }; Scope.prototype.$watch = function(watchFn, listenerFn, valueEq) { var self = this; var watcher = { watchFn: watchFn, listenerFn: listenerFn || function() { }, valueEq: !!valueEq }; self.$$watchers.push(watcher); return function() { var index = self.$$watchers.indexOf(watcher); if (index >= 0) { self.$$watchers.splice(index, 1); } }; }; Scope.prototype.$$areEqual = function(newValue, oldValue, valueEq) { if (valueEq) { return _.isEqual(newValue, oldValue); } else { return newValue === oldValue || (typeof newValue === 'number' && typeof oldValue === 'number' && isNaN(newValue) && isNaN(oldValue)); } }; Scope.prototype.$$digestOnce = function() { var self = this; var dirty; _.forEach(this.$$watchers, function(watch) { try { var newValue = watch.watchFn(self); var oldValue = watch.last; if (!self.$$areEqual(newValue, oldValue, watch.valueEq)) { watch.listenerFn(newValue, oldValue, self); dirty = true; } watch.last = (watch.valueEq ? _.cloneDeep(newValue) : newValue); } catch (e) { (console.error || console.log)(e); } }); return dirty; }; Scope.prototype.$digest = function() { var ttl = 10; var dirty; this.$beginPhase("$digest"); do { while (this.$$asyncQueue.length) { try { var asyncTask = this.$$asyncQueue.shift(); this.$eval(asyncTask.expression); } catch (e) { (console.error || console.log)(e); } } dirty = this.$$digestOnce(); if (dirty && !(ttl--)) { this.$clearPhase(); throw "10 digest iterations reached"; } } while (dirty); this.$clearPhase(); while (this.$$postDigestQueue.length) { try { this.$$postDigestQueue.shift()(); } catch (e) { (console.error || console.log)(e); } } }; Scope.prototype.$eval = function(expr, locals) { return expr(this, locals); }; Scope.prototype.$apply = function(expr) { try { this.$beginPhase("$apply"); return this.$eval(expr); } finally { this.$clearPhase(); this.$digest(); } }; Scope.prototype.$evalAsync = function(expr) { var self = this; if (!self.$$phase && !self.$$asyncQueue.length) { setTimeout(function() { if (self.$$asyncQueue.length) { self.$digest(); } }, 0); } self.$$asyncQueue.push({scope: self, expression: expr}); }; Scope.prototype.$$postDigest = function(fn) { this.$$postDigestQueue.push(fn); }; var scope = new Scope(); scope.aValue = "abc"; scope.counter = 0; var removeWatch = scope.$watch( function(scope) { return scope.aValue; }, function(newValue, oldValue, scope) { scope.counter++; } ); scope.$digest(); console.assert(scope.counter === 1); scope.aValue = 'def'; scope.$digest(); console.assert(scope.counter === 2); removeWatch(); scope.aValue = 'ghi'; scope.$digest(); console.assert(scope.counter === 2); // No longer incrementing
      
      





コンソール:

本当
本当
本当 




次は何ですか



Angularの最高の伝統で、私たちは長い道のりを歩み、スコープオブジェクトの優れた実装を作成しました。しかし、Angularのスコープオブジェクトは、私たちが持っているものよりもはるかに多くのものです。



おそらく最も重要なことは、Angularのスコープが孤立した独立したオブジェクトではないことです。それどころか、スコープオブジェクトは他のスコープから継承し、オブザーバーは、接続されているスコープのプロパティだけでなく、親スコープのプロパティも監視できます。実際、非常に単純なこのアプローチは、初心者にとって多くの問題の原因です。そのため、このシリーズの次回の記事では、スコープの継承が研究の主題になります。



将来的には、Scopeにも実装されているイベントサブシステムについても説明します。



翻訳者から:



テキストは非常に大きく、エラーやタイプミスがあると思います。個人的に送ってください-私はすべてを修正します。



誰かがハブのコードの行を選択する方法を知っている場合、たとえば、これによりコードの可読性が向上します。



All Articles