AngularJS 原始碼分析3
本文接著上一篇講
回顧
上次說到了rootScope裡的$watch方法中的解析監控表示式,即而引出了對parse的分析,今天我們接著這裡繼續挖程式碼.
$watch續
先上一塊$watch程式碼
$watch: function(watchExp, listener, objectEquality) { var scope = this, get = compileToFn(watchExp, 'watch'), array = scope.$$watchers, watcher = { fn: listener, last: initWatchVal, get: get, exp: watchExp, eq: !!objectEquality }; lastDirtyWatch = null; // in the case user pass string, we need to compile it, do we really need this ? if (!isFunction(listener)) { var listenFn = compileToFn(listener || noop, 'listener'); watcher.fn = function(newVal, oldVal, scope) {listenFn(scope);}; } if (typeof watchExp == 'string' && get.constant) { var originalFn = watcher.fn; watcher.fn = function(newVal, oldVal, scope) { originalFn.call(this, newVal, oldVal, scope); arrayRemove(array, watcher); }; } if (!array) { array = scope.$$watchers = []; } // we use unshift since we use a while loop in $digest for speed. // the while loop reads in reverse order. array.unshift(watcher); return function deregisterWatch() { arrayRemove(array, watcher); lastDirtyWatch = null; }; }
這裡的get = compileToFn(watchExp, 'watch'),上篇已經分析完了,這裡返回的是一個執行表示式的函式,接著往下看,這裡初始化了一個watcher物件,用來儲存一些監聽相關的資訊,簡單的說明一下
- fn, 代表監聽函式,當監控表示式新舊不相等時會執行此函式
- last, 儲存最後一次發生變化的監控表示式的值
- get, 儲存一個監控表示式對應的函式,目的是用來獲取表示式的值然後用來進行新舊對比的
- exp, 儲存一個原始的監控表示式
- eq, 儲存$watch函式的第三個引數,表示是否進行深度比較
然後會檢查傳遞進來的監聽引數是否為函式,如果是一個有效的字串,則通過parse來解析生成一個函式,否則賦值為一個noop佔位函式,最後生成一個包裝函式,函式體的內容就是執行剛才生成的監聽函式,預設傳遞當前作用域.
接著會檢查監控表示式是否為字串並且執行表示式的constant為true,代表這個字串是一個常量,那麼,系統在處理這種監聽的時候,執行完一次監聽函式之後就會刪除這個$watch.最後往當前作用域裡的$$watchers陣列頭中新增$watch資訊,注意這裡的返回值,利用JS的閉包保留了當前的watcher,然後返回一個函式,這個就是用來刪除監聽用的.
$eval
這個$eval也是挺方便的函式,假如你想直接在程式裡執行一個字串的話,那麼可以這麼用
$scope.name = '2'; $scope.$eval('1+name'); // ==> 會輸出12
大家來看看它的函式體
return $parse(expr)(this, locals);
其實就是通過parse來解析成一個執行表示式函式,然後傳遞當前作用域以及額外的引數,返回這個執行表示式函式的值
$evalAsync
evalAsync函式的作用就是延遲執行表示式,並且執行完不管是否異常,觸發dirty check.
if (!$rootScope.$$phase && !$rootScope.$$asyncQueue.length) { $browser.defer(function() { if ($rootScope.$$asyncQueue.length) { $rootScope.$digest(); } }); } this.$$asyncQueue.push({scope: this, expression: expr});
可以看到當前作用域內部有一個$$asyncQueue非同步佇列,儲存著所有需要延遲執行的表示式,此處的表示式可以是字串或者函式,因為這個表示式最終會呼叫$eval方法,注意這裡呼叫了$browser服務的defer方法,從ng->browser.js原始碼裡可以看到,其實這裡就是呼叫setTimeout來實現的.
self.defer = function(fn, delay) { var timeoutId; outstandingRequestCount++; timeoutId = setTimeout(function() { delete pendingDeferIds[timeoutId]; completeOutstandingRequest(fn); }, delay || 0); pendingDeferIds[timeoutId] = true; return timeoutId; };
上面的程式碼主要是延遲執行函式,另外pendingDeferIds物件儲存所有setTimeout返回的id,這個會在self.defer.cancel這裡可以取消執行延遲執行.
說digest方法之前,還有一個方法要說說
$postDigest
這個方法跟evalAsync不同的時,它不會主動觸發digest方法,只是往postDigestQueue佇列中增加執行表示式,它會在digest體內最後執行,相當於在觸發dirty check之後,可以執行別的一些邏輯.
this.$$postDigestQueue.push(fn);
下面我們來重點說說digest方法
$digest
digest方法是dirty check的核心,主要思路是先執行$$asyncQueue佇列中的表示式,然後開啟一個loop來的執行所有的watch裡的監聽函式,前提是前後兩次的值是否不相等,假如ttl超過系統預設值,則dirth check結束,最後執行$$postDigestQueue佇列裡的表示式.
$digest: function() { var watch, value, last, watchers, asyncQueue = this.$$asyncQueue, postDigestQueue = this.$$postDigestQueue, length, dirty, ttl = TTL, next, current, target = this, watchLog = [], logIdx, logMsg, asyncTask; beginPhase('$digest'); lastDirtyWatch = null; do { // "while dirty" loop dirty = false; current = target; while(asyncQueue.length) { try { asyncTask = asyncQueue.shift(); asyncTask.scope.$eval(asyncTask.expression); } catch (e) { clearPhase(); $exceptionHandler(e); } lastDirtyWatch = null; } traverseScopesLoop: do { // "traverse the scopes" loop if ((watchers = current.$$watchers)) { // process our watches length = watchers.length; while (length--) { try { watch = watchers[length]; // Most common watches are on primitives, in which case we can short // circuit it with === operator, only when === fails do we use .equals if (watch) { if ((value = 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) : value; watch.fn(value, ((last === initWatchVal) ? value : last), current); if (ttl < 5) { logIdx = 4 - ttl; if (!watchLog[logIdx]) watchLog[logIdx] = []; logMsg = (isFunction(watch.exp)) ? 'fn: ' + (watch.exp.name || watch.exp.toString()) : watch.exp; logMsg += '; newVal: ' + toJson(value) + '; oldVal: ' + toJson(last); watchLog[logIdx].push(logMsg); } } else if (watch === lastDirtyWatch) { // If the most recently dirty watcher is now clean, short circuit since the remaining watchers // have already been tested. dirty = false; break traverseScopesLoop; } } } catch (e) { clearPhase(); $exceptionHandler(e); } } } // Insanity Warning: scope depth-first traversal // yes, this code is a bit crazy, but it works and we have tests to prove it! // this piece should be kept in sync with the traversal in $broadcast if (!(next = (current.$$childHead || (current !== target && current.$$nextSibling)))) { while(current !== target && !(next = current.$$nextSibling)) { current = current.$parent; } } } while ((current = next)); // break traverseScopesLoop; takes us to here if((dirty || asyncQueue.length) && !(ttl--)) { clearPhase(); throw $rootScopeMinErr('infdig', '{0} $digest() iterations reached. Aborting!\n' + 'Watchers fired in the last 5 iterations: {1}', TTL, toJson(watchLog)); } } while (dirty || asyncQueue.length); clearPhase(); while(postDigestQueue.length) { try { postDigestQueue.shift()(); } catch (e) { $exceptionHandler(e); } } }
通過上面的程式碼,可以看出,核心就是兩個loop,外loop保證所有的model都能檢測到,內loop則是真實的檢測每個watch,watch.get就是計算監控表示式的值,這個用來跟舊值進行對比,假如不相等,則執行監聽函式
注意這裡的watch.eq這是是否深度檢查的標識,equals方法是angular.js裡的公共方法,用來深度對比兩個物件,這裡的不相等有一個例外,那就是NaN ===NaN,因為這個永遠都是false,所以這裡加了檢查
!(watch.eq ? equals(value, last) : (typeof value == 'number' && typeof last == 'number' && isNaN(value) && isNaN(last)))
比較完之後,把新值傳給watch.last,然後執行watch.fn也就是監聽函式,傳遞三個引數,分別是:最新計算的值,上次計算的值(假如是第一次的話,則傳遞新值),最後一個引數是當前作用域例項,這裡有一個設定外loop的條件值,那就是dirty = true,也就是說只要內loop執行了一次watch,則外loop還要接著執行,這是為了保證所有的model都能監測一次,雖然這個有點浪費效能,不過超過ttl設定的值後,dirty check會強制關閉,並丟擲異常
if((dirty || asyncQueue.length) && !(ttl--)) { clearPhase(); throw $rootScopeMinErr('infdig', '{0} $digest() iterations reached. Aborting!\n' + 'Watchers fired in the last 5 iterations: {1}', TTL, toJson(watchLog)); }
這裡的watchLog日誌物件是在內loop裡,當ttl低於5的時候開始記錄的
if (ttl < 5) { logIdx = 4 - ttl; if (!watchLog[logIdx]) watchLog[logIdx] = []; logMsg = (isFunction(watch.exp)) ? 'fn: ' + (watch.exp.name || watch.exp.toString()) : watch.exp; logMsg += '; newVal: ' + toJson(value) + '; oldVal: ' + toJson(last); watchLog[logIdx].push(logMsg); }
當檢查完一個作用域內的所有watch之後,則開始深度遍歷當前作用域的子級或者父級,雖然這有些影響效能,就像這裡的註釋寫的那樣yes, this code is a bit crazy
// Insanity Warning: scope depth-first traversal // yes, this code is a bit crazy, but it works and we have tests to prove it! // this piece should be kept in sync with the traversal in $broadcast if (!(next = (current.$$childHead || (current !== target && current.$$nextSibling)))) { while(current !== target && !(next = current.$$nextSibling)) { current = current.$parent; } }
上面的程式碼其實就是不斷的查詢當前作用域的子級,沒有子級,則開始查詢兄弟節點,最後查詢它的父級節點,是一個深度遍歷查詢.只要next有值,則內loop則一直執行
while ((current = next))
不過內loop也有跳出的情況,那就是當前watch跟最後一次檢查的watch相等時就退出內loop.
else if (watch === lastDirtyWatch) { // If the most recently dirty watcher is now clean, short circuit since the remaining watchers // have already been tested. dirty = false; break traverseScopesLoop; }
注意這個內loop同時也是一個label(標籤)語句,這個可以在loop中執行跳出操作就像上面的break
正常執行完兩個loop之後,清除當前的階段標識clearPhase();,然後開始執行postDigestQueue佇列裡的表示式.
while(postDigestQueue.length) { try { postDigestQueue.shift()(); } catch (e) { $exceptionHandler(e); } }
接下來說說,用的也比較多的$apply方法
$apply
這個方法一般用在,不在ng的上下文中執行js程式碼的情況,比如原生的DOM事件中執行想改變ng中某些model的值,這個時候就要使用$apply方法了
$apply: function(expr) { try { beginPhase('$apply'); return this.$eval(expr); } catch (e) { $exceptionHandler(e); } finally { clearPhase(); try { $rootScope.$digest(); } catch (e) { $exceptionHandler(e); throw e; } } }
程式碼中,首先讓當前階段標識為$apply,這個可以防止使用$apply方法時檢查是否已經在這個階段了,然後就是執行$eval方法, 這個方法上面有講到,最後執行$digest方法,來使ng中的M或者VM改變.
接下來說說scope中event模組,它的api跟一般的event事件模組比較像,提供有$on,$emit,$broadcast,這三個很實用的方法
$on
這個方法是用來定義事件的,這裡用到了兩個例項變數$$listeners, $$listenerCount,分別用來儲存事件,以及事件數量計數
$on: function(name, listener) { var namedListeners = this.$$listeners[name]; if (!namedListeners) { this.$$listeners[name] = namedListeners = []; } namedListeners.push(listener); var current = this; do { if (!current.$$listenerCount[name]) { current.$$listenerCount[name] = 0; } current.$$listenerCount[name]++; } while ((current = current.$parent)); var self = this; return function() { namedListeners[indexOf(namedListeners, listener)] = null; decrementListenerCount(self, 1, name); }; }
分析上面的程式碼,可以看出每當定義一個事件的時候,都會向$$listeners物件中新增以name為key的屬性,值就是事件執行函式,注意這裡有個事件計數,只要有父級,則也給父級的$$listenerCount新增以name為key的屬性,並且值+1,這個$$listenerCount
會在廣播事件的時候用到,最後這個方法返回一個取消事件的函式,先設定$$listeners中以name為key的值為null,然後呼叫decrementListenerCount來使該事件計數-1.
$emit
這個方法是用來觸發$on定義的事件,原理就是loop$$listeners屬性,檢查是否有值,有的話,則執行,然後依次往上檢查父級,這個方法有點類似冒泡執行事件.
$emit: function(name, args) {
var empty = [],
namedListeners,
scope = this,
stopPropagation = false,
event = {
name: name,
targetScope: scope,
stopPropagation: function() {stopPropagation = true;},
preventDefault: function() {
event.defaultPrevented = true;
},
defaultPrevented: false
},
listenerArgs = concat([event], arguments, 1),
i, length;
do {
namedListeners = scope.$$listeners[name] || empty;
event.currentScope = scope;
for (i=0, length=namedListeners.length; i<length; i++) {
// if listeners were deregistered, defragment the array
if (!namedListeners[i]) {
namedListeners.splice(i, 1);
i--;
length--;
continue;
}
try {
//allow all listeners attached to the current scope to run
namedListeners[i].apply(null, listenerArgs);
} catch (e) {
$exceptionHandler(e);
}
}
//if any listener on the current scope stops propagation, prevent bubbling
if (stopPropagation) return event;
//traverse upwards
scope = scope.$parent;
} while (scope);
return event;
}
上面的程式碼比較簡單,首先定義一個事件引數,然後開啟一個loop,只要scope有值,則一直執行,這個方法的事件鏈是一直向上傳遞的,不過當在事件函式執行stopPropagation方法,就會停止向上傳遞事件.
$broadcast
這個是$emit的升級版,廣播事件,即能向上傳遞,也能向下傳遞,還能平級傳遞,核心原理就是利用深度遍歷當前作用域
$broadcast: function(name, args) {
var target = this,
current = target,
next = target,
event = {
name: name,
targetScope: target,
preventDefault: function() {
event.defaultPrevented = true;
},
defaultPrevented: false
},
listenerArgs = concat([event], arguments, 1),
listeners, i, length;
//down while you can, then up and next sibling or up and next sibling until back at root
while ((current = next)) {
event.currentScope = current;
listeners = current.$$listeners[name] || [];
for (i=0, length = listeners.length; i<length; i++) {
// if listeners were deregistered, defragment the array
if (!listeners[i]) {
listeners.splice(i, 1);
i--;
length--;
continue;
}
try {
listeners[i].apply(null, listenerArgs);
} catch(e) {
$exceptionHandler(e);
}
}
// Insanity Warning: scope depth-first traversal
// yes, this code is a bit crazy, but it works and we have tests to prove it!
// this piece should be kept in sync with the traversal in $digest
// (though it differs due to having the extra check for $$listenerCount)
if (!(next = ((current.$$listenerCount[name] && current.$$childHead) ||
(current !== target && current.$$nextSibling)))) {
while(current !== target && !(next = current.$$nextSibling)) {
current = current.$parent;
}
}
}
return event;
}
程式碼跟$emit差不多,只是跟它不同的時,這個是不斷的取next值,而next的值則是通過深度遍歷它的子級節點,兄弟節點,父級節點,依次查詢可用的以name為key的事件.注意這裡的註釋,跟$digest裡的差不多,都是通過深度遍歷查詢,所以$broadcast方法也不能常用,效能不是很理想
$destroy
這個方法是用來銷燬當前作用域,程式碼主要是清空當前作用域內的一些例項屬性,以免執行digest,$emit,$broadcast時會關聯到
$destroy: function() { // we can't destroy the root scope or a scope that has been already destroyed if (this.$$destroyed) return; var parent = this.$parent; this.$broadcast('$destroy'); this.$$destroyed = true; if (this === $rootScope) return; forEach(this.$$listenerCount, bind(null, decrementListenerCount, this)); // sever all the references to parent scopes (after this cleanup, the current scope should // not be retained by any of our references and should be eligible for garbage collection) if (parent.$$childHead == this) parent.$$childHead = this.$$nextSibling; if (parent.$$childTail == this) parent.$$childTail = this.$$prevSibling; if (this.$$prevSibling) this.$$prevSibling.$$nextSibling = this.$$nextSibling; if (this.$$nextSibling) this.$$nextSibling.$$prevSibling = this.$$prevSibling; // All of the code below is bogus code that works around V8's memory leak via optimized code // and inline caches. // // see: // - https://code.google.com/p/v8/issues/detail?id=2073#c26 // - https://github.com/angular/angular.js/issues/6794#issuecomment-38648909 // - https://github.com/angular/angular.js/issues/1313#issuecomment-10378451 this.$parent = this.$$nextSibling = this.$$prevSibling = this.$$childHead = this.$$childTail = this.$root = null; // don't reset these to null in case some async task tries to register a listener/watch/task this.$$listeners = {}; this.$$watchers = this.$$asyncQueue = this.$$postDigestQueue = []; // prevent NPEs since these methods have references to properties we nulled out this.$destroy = this.$digest = this.$apply = noop; this.$on = this.$watch = function() { return noop; }; }
程式碼比較簡單,先是通過foreach來清空$$listenerCount例項屬性,然後再設定$parent,$$nextSibling,$$prevSibling,$$childHead,$$childTail,$root為null,清空$$listeners,$$watchers,$$asyncQueue,$$postDigestQueue,最後就是重罷方法為noop佔位函式
總結
rootScope說完了,這是個使用比例非常高的核心provider,分析的比較簡單,有啥錯誤的地方,希望大家能夠指出來,大家一起學習學習,下次有空接著分析別的.
作者宣告
本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段宣告,且在文章頁面明顯位置給出原文連線,否則保留追究法律責任的權利。
相關推薦
AngularJS 原始碼分析3
本文接著上一篇講 上一篇地址 回顧 上次說到了rootScope裡的$watch方法中的解析監控表示式,即而引出了對parse的分析,今天我們接著這裡繼續挖程式碼. $watch續 先上一塊$watch程式碼 $watch: function(watchExp, listener, objectEquali
lucene原始碼分析(3)facet例項
簡單的facet例項 public class SimpleFacetsExample { private final Directory indexDir = new RAMDirectory(); private final Directory taxoDir = new RAMD
Shiro原始碼分析(3) - 認證器(Authenticator)
本文在於分析Shiro原始碼,對於新學習的朋友可以參考 [開濤部落格](http://jinnianshilongnian.iteye.com/blog/2018398)進行學習。 Authenticator就是認證器,在Shiro中負責認證使用者提交的資訊,
CocosCreator物理引擎Demo原始碼分析(3)-stick-arrow
stick-arrow示例展示瞭如何動態發射剛體飛往目標點。 技術點 1、觸控式螢幕幕發射剛體,計算起點和目標點的夾角,設定剛體的線性速度。 2、在Update中不斷施加一個作用力到剛體尾部,使它能一直往目標點飛去。 3、在碰撞上後,動態計算並設定WeldJoin
記一次 JVM 原始碼分析(3.記憶體管理與GC)
簡介 miniJVM 的記憶體管理的實現較為簡單 記憶體分配使用了開源的 ltalloc 庫 GC就是經典的 Mark-Sweep GC 物件分配 物件分配要關注的就兩個過程 New 一個 Java 物件的過程 記憶體塊在堆上分配的過程 物件在 JVM
NodeJS原始碼分析(3)
Node Stream模組 Stream在平時業務開發時很少用到, 但是很多模組都是基於stream實現的,引用官方文件的解釋: 流(stream)在 Node.js 中是處理流資料的抽象介面(abstract interface)。 stream 模組提
OKHttp原始碼分析3
1 概述 上篇文章,我們詳細分析了OKHttp中Request的建立和傳送過程。其中sendRequest(), readResponse(), followUpRequest()三個關鍵方法在底層HttpEngine中實現。革命尚未成功,我們接下來在這篇文章
coreutils4.5.1 dirname.c原始碼分析3
老調重彈,每次先按程式碼量排序,從行數少的程式開始讀,總能有所收穫。比如,在dirname.c中,我發現幾條: 第一、函式和括號可以用空格隔開,很奇怪。如 void usage (int status) 在usage與(中有一個空格,我寫了一個測試程式,也驗證了猜想。 第二、對字元取地址,真怪異!
coreutils4.5.1 basename.c原始碼分析3
coreutils4.5.1 basename.c原始碼分析2 前幾天又重新讀了basename.c對其中去掉字尾的那段,終於理解了。現總結如下; static void remove_suffix (char *name, const char *suffix) { char *
Erlang:RabbitMQ原始碼分析 3. supervisor和supervisor2深入分析
supervisor也是Erlang/OTP裡一個常用的behavior,用於構建supervisor tree實現程序監控,故障恢復。 而RabbitMQ實現了一個supervisor2,我們從原始碼角度分析二者的實現和區別。 先介紹一些supervisor的基本概念,
LAV Filter 原始碼分析 3: LAV Video (1)
LAV Video 是使用很廣泛的DirectShow Filter。它封裝了FFMPEG中的libavcodec,支援十分廣泛的視訊格式的解碼。在這裡對其原始碼進行詳細的分析。LAV Video 工程程式碼的結構如下圖所示直接看LAV Video最主要的類CLAVVideo
AngularJS 原始碼分析2
上一篇地址 本文主要分析RootScopeProvider和ParseProvider RootScopeProvider簡介 今天這個rootscope可是angularjs裡面比較活躍的一個provider,大家可以理解為一個模型M或者VM,它主要負責與控制器或者指令進行資料互動. 今天使用的原始碼跟上次
AngularJS 原始碼分析1
AngularJS簡介 angularjs 是google出品的一款MVVM前端框架,包含一個精簡的類jquery庫,創新的開發了以指令的方式來元件化前端開發,可以去它的官網看看,請戳這裡 再貼上一個本文原始碼分析對應的angularjs原始碼合併版本1.2.4,精簡版的,除掉了所有的註釋, 請戳這裡 從啟動
vivi原始碼分析3
繼續分析vivi原始碼。 step 5: MTD裝置初始化。 關於什麼是MTD,為什麼要使用MTD,MTD技術的架構是什麼,等等,可以參考《Linux MTD原始碼分析》(作者:Jim Zeus,2002-04-29)。這份文件的參考價值比較大,猜想作者
Spring原始碼分析3 — spring XML配置檔案的解析流程
1 介紹 建立並初始化spring容器中,關鍵一步就是讀取並解析spring XML配置檔案。這個過程比較複雜,本文將詳細分析整個流程。先看涉及到的關鍵類。 XmlWebApplicationContext:web應用的預設Spring容器 XmlBean
Redis網路庫原始碼分析(3)之ae.c
一、aeCreateEventLoop & aeCreateFileEvent 上一篇文章中,我們已經將伺服器啟動,只是其中有些細節我們跳過了,比如aeCreateEventLoop函式到底做了什麼? 接下來我們要分析ae.c檔案,它是整個Redis
Django原始碼分析3:處理請求wsgi分析與檢視View
django原始碼分析 本文環境python3.5.2,django1.10.x系列 根據前上一篇runserver的博文,已經分析了本地除錯伺服器的大致流程,現在我們來分析一下當runserver執行起來後,django框架是如何處理一個請求的,djan
malloc原始碼分析---3
malloc原始碼分析—_int_malloc 上一章分析了_int_malloc的前面一小部分,本章繼續往下看, _int_malloc — fastbin static void * _int_malloc(mstate av, size_t by
AngularJS 原始碼分析4
angularjs之$compile 今天主要說說ng裡的$compile,這是一個非常關鍵的服務,頁面上的雙向繫結,各個監聽基本上都是在這裡執行的. 原始碼部分還是引用angular1.2.4,連結在這裡下載 compile的源頭 ng裡最開始引用$compile
Monkey原始碼分析3—Monkey原始碼的整體設計結構
Monkey原始碼地址,點選檢視 Monkey自動化測試的本質就是隨機生成一個事件,然後向Android設備註入一個事件。通俗的來說就是,monkey隨機生成一個點選螢幕事件,然後選取Android螢幕的一個座標,對此座標進行點選操作。來實現自動化測試的。當然產生的事件不