소개
스코프는 NG 생태계의 핵심 위치에 있습니다. NG에 의해 외부 세계에 주장 된 양방향 결합의 기본 층은 실제로 범위에 의해 구현됩니다. 이 장은 주로 범위의 시계 메커니즘, 상속 및 이벤트 구현을 분석합니다.
감시 장치
1. $ 시계
1.1 사용
// $ watch : function (WatchExp, 리스너, 객체 평등)
var unwatch = $scope.$watch('aa', function () {}, isEqual);
Angular를 사용한 사람들은 일반적으로 청취를 추가하는 "수동으로"라고 알려진 위의 코드를 종종 사용하며, 다른 청취자는 보간이나 지시를 통해 청취를 자동으로 추가하지만 원칙적으로 동일합니다.
1.2 소스 코드 분석
함수 (WatchExp, Listener, Objectequality) {var scope = this, // 가능한 문자열을 fn get = compiletofn (Watchexp, 'watch'), Array = scope. $$ watcher = {fn : listener, last : initwatchval, // 다음 비교로 기록됩니다 : get, ex, ex : ex : ex : eq : eq : eq : eq : eq. 비교 또는 가치 비교}; lastdirtywatch = null; if (! isfunction (listener)) {var listenfn = compiletofn (리스너 || 누프, '청취자'); Watcher.fn = function (NewVal, OldVal, Scope) {ListenFn (Scope);}; } if (! array) {array = scope. $$ watchers = []; } // Unshift가 푸시되지 않는 이유는 $ 다이제스트에서 Watchers 루프가 뒷면에서 시작되기 때문입니다. 새로 추가 된 감시자가 현재 루프에서 실행되도록하기 때문에 대기열의 전면에 배치됩니다. Unshift (Watcher); // return Undatchfn, 취소 청취 반환 함수 deregisterwatch () {arrayremove (Array, Watcher); lastdirtywatch = null; };}코드에서 $ watch는 비교적 간단합니다. 주로 감시자를 $$ 감시자 배열로 저장합니다
2. $ 다이제스트
스코프 값이 변경되면 스코프는 각 감시자 리스너를 자체적으로 실행하지 않습니다. 알림이 있어야 하며이 알림을 보내는 사람은 $ 다이제스트입니다.
2.1 소스 코드 분석
전체 $ 다이제스트의 소스 코드는 약 100 줄이며, 주요 논리는 더러운 체크 루프에 집중되어 있습니다. 루프 이후의 약간의 작은 코드 (예 : digestqueue의 처리와 같은 작은 코드도 있으므로 자세히 분석하지 않습니다.
Dirty Value Check Loop은 감시자의 값에 대한 업데이트가있는 한 값 업데이트가 업데이트되지 않을 때까지 검사 라운드를 실행해야 함을 의미합니다. 물론 불필요한 점검을 줄이기 위해 일부 최적화가 이루어졌습니다.
암호:
// $ Digest 루프를 입력하고 시작하여 시작한 시작가 시작하지 않도록 표시하십시오 ( '$ digest'); lastDirtyWatch = null; // 더러운 값 검사 루프는 {dirty = false; 현재 = 대상; // Asyncqueue 루프는 TraversescopesLoop를 생략합니다 : do {if ((Watchers = current. $$ Watchers)) {length = watchers.length; while (길이-) {try {watch = Watchers [length]; if (watch) {// 값이 업데이트되는지 여부를 결정하기 위해 업데이트를 작성하고 다음과 같이 분해 // value = watch.get (current), last = watch.last // value! == 마지막으로 값이라면 값을 판단하는 데 필요한 값을 만들어야하는지 여부를 결정하십시오. watch.get (current))! == (last = watch.last) &&! //이 루프의 변경 사항을 시청하는 레코드 LastDirtyWatch = Watch; // 마지막 값 watch.last = watch.eq? 복사 (값, null) : 값; // ListenERFN (NewValue, lastValue, Scope) // 첫 번째 실행이 수행되면 LastValue도 NewValue Watch.fn (value, ((last === initwatchval))로 설정됩니다. // ... WatchLog는 (Watch.get. $$ Unkatch) stableWatchescandidates.push ({Watch : Watch, Array : Watchers}); } // 이것은 감시자를 줄이는 최적화입니다. // 이전 루프에서 마지막으로 업데이트 된 시계가 변경되지 않은 경우,이 라운드에는 새로운 업데이트 된 시계가 없으면 전체 시계가 안정적이고 업데이트되지 않으며 루프가 여기에서 끝납니다. 나머지 시계는 다른 경우 (Watch === LastDirtyWatch) {dirty = false; 트래버스 스코프 루프를 파손하십시오. }}} catch (e) {clearphase (); $ ExceptionHandler (e); }}} //이 섹션은 약간 얽힌 상태이며, 실제로 깊이 우선 순회 // [b-> d, c-> e] // 실행 순서 a, b, d, c, e // 매번 첫 번째 자식을 얻습니다. 냉담한 형제가 없다면 형제가 없다면 이전 층으로 후퇴하고 층에 형제가 있는지 확인하십시오. 아니오가 있으면 시작 범위로 후퇴 할 때까지 계속 후퇴하십시오. 이 시점에서 다음 == NULL, SCOPES 루프는 다음에 종료됩니다 (! (다음 = (현재. }}} while ((current = next)); // TraversescopesLoop Break는 여기에서 직접갑니다. // 그것이 여전히 더러운 값 루프에 있는지 여부를 결정하고 최대 점검 수를 초과했는지 ttl 기본값 10 if ((dirty || asyncqueue.length) &&! (ttl-)) {clearphase (); $ rootscopeminerr ( 'Infdig', '{0} $ digest () 반복에 도달했습니다. 중단!/n' + '감시자는 지난 5 번의 반복에서 발사되었습니다 : {1}', ttl, tojson (워치 로그)); }} while (dirty || asyncqueue.length); // 루프 종료 // 마크 종료 다이제스트 루프 클리어 페 ();위 코드에는 3 층 루프가 있습니다
첫 번째 레이어는 더러운 값이 있다면 더러운 판사는 더러워지면 계속 루프를
하다 {
// ...
} while (더러운)
두 번째 층은 범위가 통과되었는지 여부를 결정합니다. 코드가 번역되었습니다. 여전히 순환되어 있지만 이해할 수 있습니다.
하다 {
// ....
if (현재. $$ childhead) {
다음 = 현재. $$ childhead;
} else if (current! == target && current. $$ nextsibling) {
다음 = 현재. $$ Nextibling;
}
while (! next && current! == target &&! (next = current. $$ nextsibling)) {
current = current. $ 부모;
}
} while (current = next);
루프 스코프 감시자의 세 번째 층
길이 = 감시자. 길이;
while (길이-) {
노력하다 {
Watch = Watchers [길이];
// ... 생략
} 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) {$ rootscope. $ digest ();}); } this. $$ asyncqueue.push ({scope : this, expression : expr});}Dirty Check가 이미 실행 중인지 또는 누군가가 $ Evalasync를 트리거했는지 판단함으로써
if (! $ rootscope. $$ phase &&! $ rootscope. $$ asyncqueue.length) $ browser.defer는 settimeout $ browser.defer (function () {// ...})를 호출하여 실행 순서를 변경해야합니다.당신이 연기를 사용하지 않는다면
function (exp) {queue.push ({scope : this, expression : exp}); 이. $ digest ();} scope. $ atalasync (fn1); scope. $ atalasync (fn2); // 결과는 // $ digest ()> fn1> $ digest ()> fn2 //입니다. 그러나 실제 효과는 달성해야합니다. $ digest ()> fn1> fn2Async의 내용은 이전 섹션 $ Digest에서 생략되었으며, 첫 번째 루프 층에 있습니다.
while (asyncqueue.length) {try {asynctask = asyncqueue.shift (); asynctask.scope. $ atal (asynctask.expression); } catch (e) {clearPhase (); $ ExceptionHandler (e); } lastdirtywatch = null;}간단하고 이해하기 쉽고 실행을 위해 Asynctask를 팝업하십시오.
그러나 여기에는 세부 사항이 있습니다. 왜 이렇게 설정됩니까? 그 이유는 다음과 같습니다. WatchX가 특정 루프에서 실행될 때 새 비 동기화가 추가되면 현재 LastDirtyWatch = WatchX가 설정됩니다. 이 작업의 실행은 후속 WatchX에서 새로운 값을 실행하게됩니다. 다음 코드가 없으면 루프는 다음 루프에서 마지막으로 LastDirtyWatch (WatchX)로 이동하고 현재 Dirty == False로 이동합니다.
lastdirtywatch = null;
여기에는 세부 사항이 있습니다. 왜 첫 번째 레벨에서 루프가 필요합니까? 상속 관계가있는 범위에는 $$ asyncqueue가 있기 때문에 루트에 모두 장착되므로 다음 레이어의 스코프 레이어에서 실행할 필요가 없습니다.
2. 상속
$ ParentsCope 및 $ Childscope와 같은 범위는 상속 될 수 있습니다. $ childscope.fn이 $ childscope에 fn 메소드가없는 경우, $ parentscope로 이동하여 방법을 찾으십시오.
필요한 속성을 찾을 때까지 레이어별로 레이어를 검색하십시오. 이 기능은 Javascipt의 프로토 타입 상속 특성을 사용하여 구현됩니다.
소스 코드 :
기능 (분리) {var childscope, child; if (isplicate) {child = new scope (); child. $ root = this. $ root; // 분리 물의 Asyncqueue 및 post digestqueue는 또한 공공의 근본이며 다른 독립적 인 어린이입니다. $$ asyncqueue = this. $$ asyncqueue; child. $$ post digestqueue = this. $$ post digestqueue; } else {if (! this. 이. $$$ 감시자 = this. $$ nextibling = this. $$ childhead = this. $$ child tail = null; 이. $$ 리스너 = {}; 이. $$ ListenErcount = {}; 이. $ id = nextuid (); 이. $$ childscopeclass = null; }; 이. $$ childscopeclass.prototype = this; } child = new this. $$ childscopeclass (); } // 다양한 아버지와 형제 관계를 설정합니다. 자녀 [ 'this'] = 아이; 자녀. $ 부모 = 이것; 아동. $$ prevsibling = this. $$ child tail; if (this. $$ childhead) {this. $$ childtail. $$ nextsibling = child; 이. $$ child tail = child; } else {this. $$ childhead = this. $$ childtail = child; } return child;}코드는 명확합니다. 주요 세부 사항은 속성이 독립적이어야하고 기본 사항을 기반으로 해야하는 속성입니다.
가장 중요한 코드 :
this.$$childScopeClass.prototype = this;
상속은 이런 식으로 실현됩니다.
3. 이벤트 메커니즘
3.1 $ on
함수 (이름, 청취자) {var namedListeners = this. $$ 리스너 [이름]; if (! pomendListeners) {this. $$ 리스너 [name] = namedListeners = []; } pellingListEners.push (리스너); var current = 이것; do {if (! current. $$ ListenErcount [name]) {current. $$ ListenErcount [name] = 0; } current. $$ ListenErcount [name] ++; } while ((current = current. $ parent)); var self = 이것; return function () {pamedListeners [indexof (namedListeners, Listener)] = null; retementListenerCount (self, 1, name); };}$ WATHC와 유사하게, 그것은 또한 명명 된 listeners 인 배열에 저장되어 있습니다.
범위와 모든 부모가 이벤트 통계를 저장하는 또 다른 차이점이 있습니다. 이는 이벤트 통계를 방송하고 후속 분석 할 때 유용합니다.
var current = this; do {if (! current. $$ listenerCount [name]) {current. $$ listenerCount [name] = 0; } current. $$ ListenErcount [name] ++;} while ((current = current. $ parent));3.2 $ 방출
$ Emit은 상향 방송 이벤트입니다. 소스 코드 :
function (name, args) {val empty = [], pamedListeners, scope = this, stoppropagation = false = event = {name : name : targetScope : spope, stopPropagation : function () {stopPropagation = true;}, buffic () {event.defaultPrevented = true; }, defaultPrevented : false}, warnerArgs = concat ([이벤트], 인수, 1), i, 길이; do {namedlisteners = scope. $$ 리스너 [이름] || 비어 있는; event.currentscope = 범위; for (i = 0, length = namedlisteners.length; i <length; i ++) {// 제거 된 후에는 배열에서 삭제되지 않지만 NULL로 설정되므로 (! pamedListEners.splice (i, 1); 나--; 길이--; 계속하다; } try {pamenListEners [i] .apply (null, hareserArgs); } catch (e) {$ ExceptionHandler (e); }} // 전파가 중지되면 (stopPropagation) {event.currentscope = null; 반환 이벤트; } // Emit은 상향 범위를 전파하는 방법입니다. = SCOPE. $ parent; } while (범위); event.currentscope = null; 반환 이벤트;}3.3 $ 방송
$ Broadcast는 내면을 전파하고 있습니다. 즉, 자식으로 전파, 소스 코드 :
function (name, args) {var target = this, current = target, next = target, event = {name : targetscope : target, extenceDefault : function () {event.defaultPrevented = true; }, defaultprevented : false}, warnerArgs = concat ([이벤트], 인수, 1), 청취자, i, 길이; while ((current = next)) {event.currentscope = current; 리스너 = 현재. $$ 리스너 [이름] || []; for (i = 0, length = listeners.length; i <length; i ++) {// 청취가 취소되었는지 확인하십시오 (! 리스너 [i]) {청취자.splice (i, 1); 나--; 길이--; 계속하다; } try {리스너 [i] .apply (null, hareserArgs); } catch (e) {$ ExceptionHandler (e); }} // if (next = ((현재. $$ listenerCount [name] && current. $$ childhead)) || (현재! == target && current. $$ nextsibling)))))) {while (current! == target &&! }}} event.currentscope = null; 반환 이벤트;}다른 논리는 비교적 간단합니다. 즉, 깊이있는 코드가 더 혼란 스럽습니다. 실제로, 그것은 digest와 동일합니다. 경로에 듣고 있는지 판단하는 것입니다. 현재. $$ ListenErcount [이름]. 코드의 위 $에서, 우리는 경로에 자식이있는 한 경로 헤더에 숫자가 있음을 알 수 있습니다. 반대로, 경로의 모든 어린이는 청취 이벤트가 없다고 언급되지 않은 경우.
if (! }}
전파 경로 :
루트> [a> [a1, a2], b> [b1, b2> [C1, C2], B3]]
루트> a> a1> a2> b> b1> b2> c1> c2> b3
4. $ watcholection
4.1 예제를 사용하십시오
$ scope.names = [ 'itor', 'matias', 'misko', 'James']; $ scope.datacount = 4; $ scope. $ watchCollection ( 'names', function (newNames, OldNames) {$ scope.datacount = newnames.length;}); excling ($ scope.datacount) .toequal (4); $ scope. $ digest (); excling ($ scope.datacount) .toequal (4); $ scope.names.pop (); $ scope. $ digest (); excop ($ scope.datacount (3);4.2 소스 코드 분석
함수 (OBJ, Listener) {$ watchCollectionInterceptor. $ stateful = true; var self = 이것; var newValue; var OldValue; var veryoldValue; var trackveryoldValue = (Listener.Length> 1); var 변경된 = 0; var changeetector = $ parse (OBJ, $ watchCollectionInterceptor); var internalarray = []; var internalobject = {}; var initrun = true; var oldlength = 0; // 반환 된 변경된 기능을 기준으로 변경 여부를 결정하십시오. $ watchCollectionInterceptor (_value) {// ... return changedEtected; } //이 메소드를 통해 프록시 함수로 실제 리스너를 호출하십시오. $ watchCollectionAction () {} this. $ watch (changeetector, $ watchCollectionAction);}을 반환합니다.메인 정맥은 위에서 가로 채는 코드의 일부입니다. 다음은 주로 $ watchCollectionInterceptor 및 $ watchCollectionAction을 분석합니다
4.3 $ WatchCollectionInterceptor
함수 $ watchCollectionInterceptor (_value) {newValue = _value; var newlength, Key, Bothnan, Newitem, Olditem; if (isundefined (newValue)) 반환; if (! isobject (newValue)) {if (OldValue! == newValue) {OldValue = newValue; 변경된 ++; }} else if (isArraylike (newValue)) {if (OldValue! == indernArray) {OldValue = internalArray; Oldlength = OldValue.length = 0; 변경된 ++; } newlength = newValue.length; if (oldlength! == newlength) {changeetected ++; 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)) {changeetected ++; OldValue [i] = newitem; }}} else {if (OldValue! == internalObject) {OldValue = internalObject = {}; oldlength = 0; 변경된 ++; } newlength = 0; for (newValue의 키) {if (hasownProperty.call (newValue, key)) {Newlength ++; newitem = newValue [키]; olditem = OldValue [키]; if (OldValue의 키) {bothnan = (olditem! == Olditem) && (newitem! == newitem); if (! bothnan && (olditem! == newitem)) {changeetected ++; OldValue [key] = newitem; }} else {Oldlength ++; OldValue [key] = newitem; 변경된 ++; }}} if (oldlength> newlength) {changeetected ++; for (OldValue의 키) {if (! hasownProperty.call (newValue, key)) {Oldlength--; OldValue 삭제 [키]; }}}} return changeetected;}1). 값이 정의되지 않은 경우 직접 반환하십시오.
2). 값이 일반적인 기본 유형 인 경우 그 값이 동일인지 직접 결정하십시오.
3). 값이 클래스 배열 인 경우 (즉, 길이 속성이 존재하고, 값 [i]가 클래스 배열이라고도 함), OldValue는 초기화없이 먼저 초기화됩니다.
if (OldValue! == internalArray) {OldValue = internalArray; Oldlength = OldValue.length = 0; 변경된 ++;}그런 다음 배열 길이를 비교하십시오. 동일하지는 않지만 변경된 검출 된 ++로 기록됩니다.
if (oldlength! == newlength) {changeetected ++; 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)) {changeetected ++; OldValue [i] = newitem; }}4). 값이 객체 인 경우 초기화 프로세스는 위와 유사합니다.
if (OldValue! == internalObject) {OldValue = internalObject = {}; oldlength = 0; 변경된 ++;}다음 처리가 더 능숙합니다. 많은 NewValue가있는 새 필드가 추가 된 경우 Oldlength에 1을 추가하여 Oldlength가 추가되고 빼지 않도록하십시오. NewValue에 새로운 필드가 있는지 쉽게 찾을 수 있습니다. 마지막으로, OldValue의 추가 필드, 즉 NewValue의 삭제 된 필드를 제거한 다음 끝났습니다.
newlength = 0; for (newValue의 키) {if (hasownproperty.call (newValue, key)) {Newlength ++; newitem = newValue [키]; olditem = OldValue [키]; if (OldValue의 키) {bothnan = (olditem! == Olditem) && (newitem! == newitem); if (! bothnan && (olditem! == newitem)) {changeetected ++; OldValue [key] = newitem; }} else {Oldlength ++; OldValue [key] = newitem; 변경된 ++; }}} if (oldlength> newlength) {changeetected ++; for (OldValue의 키) {if (! hasownProperty.call (newValue, key)) {Oldlength--; OldValue 삭제 [키]; }}}4.4 $ WatchCollectionAction
함수 $ watchColleCtionAction () {if (initRun) {initRun = false; 리스너 (NewValue, NewValue, Self); } else {Listener (NewValue, hotoldValue, self); } // trackveryOldValue = (Learger.Length> 1) 청취자 메소드에 If (trackveryOldValue) {if (! isobject (newValue)) {veryoldValue = newValue; } else if (isArraylike (newValue)) {veryoldValue = new Array (newValue.Length); for (var i = 0; i <newValue.length; i ++) {veryoldValue [i] = newValue [i]; }} else {veryoldValue = {}; for (newValue의 var key) {if (hasownProperty.call (newValue, key)) {veryoldValue [key] = newValue [key]; }}}}}}코드는 비교적 간단하며 LearerFn을 호출하는 것입니다. 첫 번째 호출이 OldValue == NewValue 일 때. 효율성과 메모리의 경우 리스너가 OldValue 매개 변수가 필요한지 여부가 결정됩니다.
5. $ Eval & $ 적용
$ eval : function (expr, locals) {return $ parse (expr) (this, locals);}, $ apply : function (expr) {try {beginphase ( '$ apply'); 이것을 반환합니다. $ atpl (expr); } catch (e) {$ ExceptionHandler (e); } 마침내 {clearPhase (); try {$ rootscope. $ digest (); } catch (e) {$ ExceptionHandler (e); e 던지기; }}}$ apply는 마지막으로 $ rootscope를 호출합니다. $ digest ()는 $ apply ()를 호출하는 대신 $ digest ()를 사용하는 것이 더 효율적입니다.
주요 논리는 모두 $ parse로되어 있으며 구문 구문 분석 기능에 속하며 향후 개별적으로 분석됩니다.