導入
スコープは、NGエコシステムのコア位置にあります。 Ngが外の世界に主張する双方向結合の基礎となる層は、実際には範囲によって実装されています。この章では、主にスコープの時計メカニズム、継承、イベントの実装を分析します。
モニター
1。$ watch
1.1使用
// $ watch:function(watchexp、リスナー、オブジェクトエクリティ)
var unwatch = $scope.$watch('aa', function () {}, isEqual);
Angularを使用した人は、多くの場合、上記のコードを使用します。これは、一般に「手動で」リスニングを追加することとして知られています。他のリスナーの一部は、補間または指令を通じて自動的にリスニングを追加しますが、原則として同じです。
1.2ソースコード分析
function(watchexp、リスナー、オブジェクトエクリティ){var scope = this、//可能な文字列をfn get = compiletofn(watchexp、 'watch')、array = scope。$$ watcher = {fn:reaster、last:initwatchal、// last last valueは次の比較を記録するために記録されます: 比較}; lastdirtywatch = null; if(!isfunction(ristener)){var ristenfn = cypiletofn(listener || noop、 'listeren'); watcher.fn = function(newval、oldval、scope){ristenfn(scope);}; } if(!array){array = scope。$$ watchers = []; } // Unshiftがプッシュされない理由は、$ DIGESTでウォッチャーループが背面から起動するためです。 // return waTchfn、リスニングリターン機能deregisterwatch(){arrayremove(array、watcher); lastdirtywatch = null; };}コードから、$ watchは比較的簡単です。主にウォッチャーを$$ウォッチャーアレイに保存します
2。$ダイジェスト
スコープ値が変更された場合、スコープは各ウォッチャーリスナーを単独で実行しません。通知が必要であり、この通知を送信する人は$ DIGESTです
2.1ソースコード分析
$ digest全体のソースコードは約100行で、主なロジックは汚れたチェックループに集中しています。また、ループの後には、ポストデイジェストキューの処理など、いくつかのマイナーコードがあるため、詳細に分析しません。
ダーティバリューチェックループとは、ウォッチャーの値の更新がある限り、値の更新が更新されるまでチェックのラウンドを実行する必要があることを意味します。もちろん、不必要なチェックを減らすためにいくつかの最適化が行われました。
コード:
// $ digestループを入力してマークを付けて、beginphase( '$ digest'); lastdirtywatch = null; // Dirty Value Check Loop Starts do {dirty = false; // current =ターゲット; // asyncqueueループはtraversescopesloopを省略します:do {if((watchers = current。$$ watchers)){length = watchers.length; while(length-){try {watch = watchers [length]; if(watch){//アップデートを作成して、値が更新されているかどうかを判断し、次のように分解// value = watch.get(current)、last = watch.last.last // value!==それが真である場合は最後に、watch.eq?equals(value、last)// falue of nan!= nan = nan = nan = nan = nan = nanの判断を決定する必要があるかどうかを判断します。 watch.get(current)!==(last = watch.last)&&!(watch.eq?equals(value、last):( typeof value === 'number' && typeof last == 'number' && isnan(value)&& isnan(last))){dirty = true; //このループで監視が変更された記録lastdirtywatch = watch; //最後の値watch.last = watch.eq? copy(value、null):value; // rienseRfn(newValue、lastValue、scope)//最初の実行が実行された場合、lastValueはnewValue watch.fn((last === initwatchval)?value:last)、currentにも設定されます。 // ... watchlogはif(watch.get。$$ locatch)stablewatchescandidates.push({watch:watch、array:watchers}); } //これは、ウォッチャーを減らす最適化です//前のループで最後に更新された時計が変更されていない場合、つまり、このラウンドに新しい更新時計はありません//それは、時計全体が安定しており、更新されず、ループはここで終了することを意味します。残りの時計は、elseをチェックする必要はありません。トラベルズコープループを破壊します。 }}} catch(e){clearphase(); $ ExceptionHandler(e); }}} //このセクションは少しもつれています。これは、実際に深さ第一トラバーサル// a-> [b-> d、c-> e] //実行注文a、b、d、c、e //毎回最初の子供を取得します。隣の兄弟がいない場合、兄弟がいない場合は、前のレイヤーに退き、レイヤーに兄弟がいるかどうかを判断します。いない場合は、開始範囲に退却するまで後退し続けます。この時点で、次の== nullなので、スコープループは(!( }}} while((current = next)); // raverseScopesloopをここに直接壊す//それがまだ汚れたバリューループにあり、最大チェック数を超えているかどうかを判断します。 $ rootscopeminerr( 'infdig'、 '{0} $ digest()iterationsが到達しました。 }} while(dirty || asyncqueue.length); // loopの終わり//マークエグジットダイジェストループClearphase();上記のコードには3層ループがあります
最初のレイヤーは汚れている、汚れた値がある場合は、ループを続けます
する {
// ...
} while(汚れ)
2番目の層は、スコープが通過したかどうかを決定します。コードが翻訳されています。まだ流通していますが、理解することができます。
する {
// ....
if(current。$$ childhead){
next = current。$$ CHILDHEAD;
} else if(current!== target && current。$$ nextsibling){
next = current。$$ nextsibling;
}
while(!next && current!==ターゲット&&!(next = current。$$ nextsibling)){
current = current。$ parent;
}
} while(current = next);
ループスコープウォッチャーの3番目の層
length = watchers.length;
while(length-){
試す {
watch = watchers [length];
// ... 省略
} catch(e){
clearphase();
$ ExceptionHandler(e);
}
}
3。$ evalasync
3.1ソースコード分析
$ evalasyncは、実行の遅延に使用されます。ソースコードは次のとおりです。
function(expr){if(!$ rootscope。$$ phase &&!$ rootscope。$$ asyncqueue.length){$ browser.defer(function(){if($ rootscope。$$ asyncqueue.length){$ rotscope。$ digest();}); } this。$$ asyncqueue.push({scope:this、expression:expr});}汚いチェックがすでに実行されているのか、誰かが$ evalasyncをトリガーしているのかを判断することにより
if(!$ rootscope。$$ phase &&!$ rootscope。$$ asyncqueue.length)$ browser.deferは、settimeout $ browser.defer(function(){// ...})を呼び出して実行順序を変更します。Deferを使用していない場合は、
function(exp){queue.push({scope:this、expression:exp}); $ digest();} scope。$ evalasync(fn1); scope。$ evalasync(fn2); //結果は// $ digest()> fn1> $ digest()> fn2 //ですが、実際の効果を達成する必要があります:$ digest()> fn1> fn2Asyncの内容は、ループの最初の層にある前のセクション$ digestで省略されました
while(asyncqueue.length){try {asynctask = asyncqueue.shift(); asynctask.scope。$ eval(asynctask.Expression); } catch(e){clearphase(); $ ExceptionHandler(e); } lastdirtywatch = null;}シンプルで理解しやすい、実行するためにAsynctaskをポップアップします。
しかし、ここには詳細がありますが、なぜこのようにセットアップされていますか?その理由は次のとおりです。 watchxが特定のループで実行されたときに新しいAsynctaskが追加された場合、lastdirtywatch = watchxは現時点で設定されます。このタスクの実行により、Watchxのその後のWatchで新しい値が実行されます。次のコードがない場合、ループは次のループからジャンプしてlastdirtywatch(watchx)になり、この時点でdirty == falseになります。
lastdirtywatch = null;
ここにも詳細がありますが、なぜ最初のレベルでループしますか?相続関係の範囲には$$ asyncqueueがあるため、すべてルートに取り付けられているため、次のレイヤーのスコープレイヤーで実行する必要はありません。
2。継承
Scopeは、$ Parentscopeや$ Childsscope 2つのスコープなど、継承可能です。 $ childscope.fnが$ chuldscopeにFNメソッドがない場合に呼び出される場合は、$ parentscopeにアクセスしてメソッドを見つけます。
必要な属性が見つかるまでレイヤーごとに検索します。この機能は、Javasciptのプロトタイプ継承の特性を使用して実装されています。
ソースコード:
function(asealte){var childscope、child; if(asealte){child = new Scope();子。$ root = this。$ root; //分離株のasyncqueueとpostdigestqueueも公共の根と他の独立した子供です。$$ asyncqueue = this。$$ asyncqueue;子。$$ postdigestqueue = this。$$ postdigestqueue; } else {if(!this。$$ childscopeclass){this。$$ childscopeclass = function(){//ここでは、$$監視者などの分離に固有の属性を見ることができます。 this。$$$ watchers = this。$$ nextsibling = this。$$ childhead = this。$$ childtail = null;これ。$$リスナー= {}; this。$$ ristenercount = {}; this。$ id = nextuid();これ。$$ childscopeclass = null; };これ。$$ childscopeclass.prototype = this; } child = new this。$$ childscopeclass(); } //さまざまな父子と兄弟の関係を設定します。これは非常に乱雑です! child ['this'] = child;子。$ parent = this;子。$$ prevsibling = this。$$ childtail; if(this。$$ childhead){this。$$ childtail。$$ nextsibling = child;これ。$$ CHILDTAIL = CHILD; } else {this。$$ childhead = this。$$ chidetail = child; } return child;}コードは明確で、主な詳細は、どの属性を独立させる必要があり、どの属性が基本に基づいている必要があるかです。
最も重要なコード:
this.$$childScopeClass.prototype = this;
このように継承が実現されます。
3。イベントメカニズム
3.1 $ on
function(name、listerers){var namedListeners = this。$$リスナー[name]; if(!namedListeners){this。$$ ristens [name] = namedListeners = []; } namedlisteners.push(リスナー); var current = this; do {if(!current。$$ ristenercount [name]){current。$$ shulistercount [name] = 0; } current。$$ ristenercount [name] ++; } while((current = current。$ parent)); var self = this; return function(){namedlisteners [indexof(namedListeners、リスナー)] = null; Decrentlistenercount(self、1、name); };}$ wathcと同様に、アレイに保存されています-listeners。
スコープとすべての親がイベント統計を保存することになる別の違いがあります。これは、イベントを放送し、その後の分析を放送するときに役立ちます。
var current = this; do {if(!current。$$ listenercount [name]){current。$$ ristenercount [name] = 0; } current。$$ ristenercount [name] ++;} while((current = current。$ parent);3.2 $ emit
$ emitは上向きの放送イベントです。ソースコード:
function(name、args){var empty = []、namedlisteners、scope = this、stoppropagation = false = {name:name、stargencope:scope、spotpropagation:function(){stoppropagation = true;}、preventdefault:function(){event.defaultprevented = true; }、defaultPrevented:false}、ristenerargs = concat([event]、arguments、1)、i、length; do {namedlisteners = scope。$$リスナー[名前] ||空の; event.currentscope = scope; (i = 0、length = namedlisteners.length; i <length; i ++){// remodを聞いた後、配列から削除されることはありませんが、nullに設定されます。私 - ;長さ - ;続く; } try {namedlisteners [i] .apply(null、ristenerargs); } catch(e){$ exceptionhandler(e); }} //伝播が停止した場合、if(stoppropagation){event.currentscope = null;を返します。戻りイベント。 } //エミットは、上向きのスコープ= scope。$ parentを伝播する方法です。 } while(scope); event.currentscope = null;戻りイベント;}3.3 $放送
$ブロードキャストは内向きに伝播しています。つまり、子に伝播する、ソースコード:
function(name、args){var target = this、current = target、next = event = {name:name、targetscope:target、preventdefault:function(){event.defaultPrevented = true; }、defaultPrevented:false}、ristenerargs = concat([event]、arguments、1)、リスナー、i、長さ; while((current = next)){event.currentscope = current;リスナー= current。$$リスナー[名前] || []; for(i = 0、length = ristens.length; i <length; i ++){//リスニングがキャンセルされているかどうかを確認してください私 - ;長さ - ;続く; } try {リスナー[i] .apply(null、ristenerargs); } catch(e){$ exceptionhandler(e); }} // if(next =((current。$$ listenercount [name] && current。$$ childhead)||(current!== target && current。$$ nextsibling))))){while(current!==ターゲット&&!(next = current。 }}} event.currentscope = null;戻りイベント;}他のロジックは比較的単純です。つまり、詳細に通過するコードはより混乱しています。実際、それはダイジェストと同じです。道に耳を傾けるかどうかを判断することです。現在。コード上の上記の$から、パスに子供がいる限り、パスヘッダーにも数字があることがわかります。それどころか、道にいるすべての子供にはリスニングイベントがないと述べられていない場合。
if(!(!next =((current。$$ listener)[name] [name] && current。$$ childhead)||(current!== target && current。$$ nextsibling))))))))))))))))))){ }}伝播パス:
root> [a> [a1、a2]、b> [b1、b2> [c1、c2]、b3]]]
root> a> a1> a2> b> b1> b2> c1> c2> b3
4。$ watchcollection
4.1例を使用します
$ scope.names = ['igor'、 'matias'、 'misko'、 'james']; $ scope.datacount = 4; $ scope。$ watchcollection( 'names'、function(newnames、oldnames){$ scope.datacount = newnames.length;}); expect($ scope.datacount).toequal(4); $ scope。$ digest(); expect($ scope.datacount).toequal(4); $ scope.names.pop(); $ scope。4.2ソースコード分析
function(obj、リスナー){$ watchcollectionInterceptor。$ stateful = true; var self = this; var newValue; var oldvalue; var velyoldvalue; var trackveryoldvalue =(listener.length> 1); var chandereTected = 0; var chandereTector = $ parse(obj、$ watchCollectionInterceptor); var internalArray = []; var internalObject = {}; var initrun = true; var oldlength = 0; //返された変更された関数に基づいて変更するかどうかを決定します$ watchcollectionInterceptor(_value){// ... return chanderetected; } //このメソッドを介して実際のリスナーをプロキシ関数として呼び出します$ watchcollectionaction(){} this。$ watch(chandereTector、$ watchcollectionaction);}主な静脈は、上記のコードの一部です。以下は、主に$ watchCollectionInterceptorと$ watchCollectionactionを分析します
4.3 $ watchCollectionInterceptor
function $ watchCollectionInterceptor(_Value){newValue = _Value; var newlength、key、bothnan、newitem、olditem; if(isundefined(newValue))return; if(!isobject(newValue)){if(oldValue!== newValue){oldValue = newValue;検出された++を変更しました。 }} else if(isarraylike(newValue)){if(oldValue!== internalArray){oldvalue = internalArray; oldlength = oldvalue.length = 0;検出された++を変更しました。 } newLength = newValue.length; if(oldlength!== newLength){chandereTected ++; oldvalue.length = oldlength = newlength; } for(var i = 0; i <newlength; i ++){olditem = oldvalue [i]; newItem = newValue [i]; bothnan =(olditem!== olditem)&&(newitem!== newItem); if(!bothnan &&(olditem!== newItem)){chandereTected ++; OldValue [i] = newItem; }}} else {if(oldvalue!== internalObject){oldValue = internalObject = {}; oldlength = 0;検出された++を変更しました。 } newLength = 0; for(key in newValue){if(hasownProperty.call(newValue、key)){newLength ++; newItem = newValue [key]; OldItem = OldValue [key]; if(key in oldvalue){bothnan =(olditem!== olditem)&&(newItem!== newItem); if(!bothnan &&(olditem!== newItem)){chandereTected ++; OldValue [key] = newItem; }} else {oldlength ++; OldValue [key] = newItem;検出された++を変更しました。 }}} if(oldlength> newlength){chandereTected ++; for(key in OldValue){if(!hasownProperty.call(newValue、key)){oldlength--; OldValue [key]を削除します。 }}}} return chandereTected;}1)。値が定義されていない場合は、直接戻ります。
2)。値が通常の基本タイプである場合、それが等しいかどうかを直接決定します。
3)。値がクラスアレイ(つまり、長さの属性が存在し、値[i]がクラスアレイとも呼ばれます)である場合、oldvalueは初期化なしで最初に初期化されます
if(oldvalue!== internalArray){oldvalue = internalArray; oldlength = oldvalue.length = 0;検出された++;}次に、配列の長さを比較します。等しくない場合、変更された変更++の変更として記録されます
if(oldlength!== newLength){chandereTected ++; oldvalue.length = oldlength = newlength;}1つずつ比較します
for(var i = 0; i <newlength; i ++){olditem = oldvalue [i]; newItem = newValue [i]; bothnan =(olditem!== olditem)&&(newitem!== newItem); if(!bothnan &&(olditem!== newItem)){chandereTected ++; OldValue [i] = newItem; }}4)。値がオブジェクトの場合、初期化プロセスは上記に似ています
if(oldvalue!== internalObject){oldValue = internalObject = {}; oldlength = 0;検出された++;}次の処理はより熟練しています。多くのNewValueを備えた新しいフィールドが追加されていることがわかった場合は、1つをOldLengthに追加して、Old Lengthが追加して減少しないようにします。 NewValueに新しいフィールドがあるかどうかを簡単に見つけることができます。最後に、OldValueの追加フィールド、つまりNewValueの削除されたフィールドを削除してから、終了します。
newLength = 0; for(key in newValue){if(hasownProperty.call(newValue、key)){newLength ++; newItem = newValue [key]; OldItem = OldValue [key]; if(key in oldvalue){bothnan =(olditem!== olditem)&&(newItem!== newItem); if(!bothnan &&(olditem!== newItem)){chandereTected ++; OldValue [key] = newItem; }} else {oldlength ++; OldValue [key] = newItem;検出された++を変更しました。 }}} if(oldlength> newlength){chandereTected ++; for(key in OldValue){if(!hasownProperty.call(newValue、key)){oldlength--; OldValue [key]を削除します。 }}}4.4 $ watchcollectionaction
function $ watchcollectionaction(){if(initrun){initrun = false;リスナー(newValue、newValue、self); } else {リスナー(newValue、velyoldValue、self); } // trackveryoldValue =(listener.length> 1)リスナーメソッドがoldvalueを必要とするかどうかを確認します//必要に応じてコピー(trackveryoldvalue){if(!isobject(newValue)){bealoldValue = newValue; } else if(isarraylike(newValue)){vealoldValue = new Array(newValue.length); for(var i = 0; i <newValue.length; i ++){velyoldValue [i] = newValue [i]; }} else {velyoldvalue = {}; for(newValueのvar key){if(hasownproperty.call(newValue、key)){vealoldValue [key] = newValue [key]; }}}}}}コードは比較的単純で、listenerfnを呼び出すことです。最初の呼び出しがoldvalue == newValueの場合。効率とメモリのために、リスナーがoldValueパラメーターを必要とするかどうかが決定されます。
5。$ eval&$ apply
$ eval:function(expr、locals){return $ parse(expr)(this、locals);}、$ apply:function(expr){try {beginphase( '$ apply');これを返します。$ eval(expr); } catch(e){$ exceptionhandler(e); }最後に{clearphase(); {$ rotscope。$ digest(); } catch(e){$ exceptionhandler(e); eを投げる; }}}$ applyが最終的に$ rootscope。$ digest()を呼び出します。そのため、$ apply()を呼び出す代わりに$ digest()を使用することをお勧めします。これはより効率的です。
主なロジックはすべて$ parseであり、これは構文解析機能に属し、将来的に個別に分析されます。