MVVM大比拼之AngularJS源碼精析javascript
簡介html
AngularJS的學習資源已經很是很是多了,AngularJS基礎請直接看官網文檔。這裏推薦幾個深度學習的資料:vue
其實隨便google一下就會看到很是的多的AngularJS的深度文章,AngularJS的開發團隊自己對外也很是活躍。特別是如今AngularJS 2.0也在火熱設計和開發中,你們徹底能夠把握這個機會跟進一下。設計文檔在這裏。在這些資料面前,個人源碼分析只能算是班門弄斧了。不過人總要本身思考,不然和鹹魚沒有區別。 如下源碼以1.3.0爲準。java
入口git
除了使用 ng-app,angular還有手工的入口:angularjs
angular.bootstrap(document,['module1','module2'])
angularJS build的相關信息和文件結構翻閱一下gruntFile就清楚了。咱們直擊/src/Angular.js 的1381行 bootstrap 定義:github
function bootstrap(element, modules, config) { if (!isObject(config)) config = {}; var defaultConfig = { strictDi: false }; config = extend(defaultConfig, config); var doBootstrap = function() { element = jqLite(element); if (element.injector()) { var tag = (element[0] === document) ? 'document' : startingTag(element); throw ngMinErr('btstrpd', "App Already Bootstrapped with this Element '{0}'", tag); } modules = modules || []; modules.unshift(['$provide', function($provide) { $provide.value('$rootElement', element); }]); modules.unshift('ng'); var injector = createInjector(modules, config.strictDi); injector.invoke(['$rootScope', '$rootElement', '$compile', '$injector', '$animate', function(scope, element, compile, injector, animate) { scope.$apply(function() { element.data('$injector', injector); compile(element)(scope); }); }] ); return injector; }; var NG_DEFER_BOOTSTRAP = /^NG_DEFER_BOOTSTRAP!/; if (window && !NG_DEFER_BOOTSTRAP.test(window.name)) { return doBootstrap(); } window.name = window.name.replace(NG_DEFER_BOOTSTRAP, ''); angular.resumeBootstrap = function(extraModules) { forEach(extraModules, function(module) { modules.push(module); }); doBootstrap(); }; }
已經熟練使用AngularJS的讀者應該立刻就注意到,代碼中部的createInjector和後面的幾行代碼就已經暴露了兩個核心概念的入口:「依賴注入」和「視圖編譯」。bootstrap
依賴注入api
先不要急着去看 createInjector 的定義, 先看看後面這一句 injector.invoke()。在angular中有顯式注入和隱式注入,這裏是顯式。往 invoke 中傳如的參數是個數組,數組前n-1個參數對應着對最後一個函數的每個參數,也就是最後一個函數中要傳入的依賴。不難猜測,injector應該是個對象,其中保存了全部已經實例化過的service等能夠做爲依賴的函數或對象,調用invoke時就會按名字去取依賴。如今讓咱們去驗證吧。翻到 /src/auto/injector.js 609:數組
function createInjector(modulesToLoad, strictDi) { strictDi = (strictDi === true); var INSTANTIATING = {}, providerSuffix = 'Provider', path = [], loadedModules = new HashMap(), providerCache = { $provide: { provider: supportObject(provider), factory: supportObject(factory), service: supportObject(service), value: supportObject(value), constant: supportObject(constant), decorator: decorator } }, providerInjector = (providerCache.$injector = createInternalInjector(providerCache, function() { throw $injectorMinErr('unpr', "Unknown provider: {0}", path.join(' <- ')); }, strictDi)), instanceCache = {}, instanceInjector = (instanceCache.$injector = createInternalInjector(instanceCache, function(servicename) { var provider = providerInjector.get(servicename + providerSuffix); return instanceInjector.invoke(provider.$get, provider, undefined, servicename); }, strictDi)); forEach(loadModules(modulesToLoad), function(fn) { instanceInjector.invoke(fn || noop); }); return instanceInjector; /*下面省略若干函數定義*/ }
咱們從最後的返回值看到,真實的injector對象又是由 createInternalInjector 創造的。只不過最後對於全部須要加載的模塊(也就是參數modulesToLoad),主動使用instanceInjector.invoke執行了一次。明顯這個invoke和前面講到的invoke是同一個函數,可是前面傳的參是數組,用來顯示傳入依賴,這裏傳的參看起來是函數,那頗有多是隱式注入的調用。 另外值得注意的是這裏有個 providerInjector 也是用 createInternalInjector 創造的。它在instancInjector 的 createInternalInjector 中被用到了。
下面讓咱們看看 createInternalInjector :
function createInternalInjector(cache, factory) { function getService(serviceName) { /*省略*/ } function invoke(fn, self, locals, serviceName){ if (typeof locals === 'string') { serviceName = locals; locals = null; } var args = [], $inject = annotate(fn, strictDi, serviceName), length, i, key; for(i = 0, length = $inject.length; i < length; i++) { key = $inject[i]; if (typeof key !== 'string') { throw $injectorMinErr('itkn', 'Incorrect injection token! Expected service name as string, got {0}', key); } args.push( locals && locals.hasOwnProperty(key) ? locals[key] : getService(key) ); } if (!fn.$inject) { // this means that we must be an array. fn = fn[length]; } // http://jsperf.com/angularjs-invoke-apply-vs-switch // #5388 return fn.apply(self, args); } function instantiate(Type, locals, serviceName) { /*省略*/ } return { invoke: invoke, instantiate: instantiate, get: getService, annotate: annotate, has: function(name) { return providerCache.hasOwnProperty(name + providerSuffix) || cache.hasOwnProperty(name); } }; }
咱們快先看看以前對 invoke 函數的猜想是否正確,咱們前面看到了調用它時第一個參數爲數組或者函數,若是你記性不錯的話,應該也注意到前面還有一句:
instanceInjector.invoke(provider.$get, provider, undefined, servicename)
好,咱們來看 invoke。注意 $inject = annotate(fn, strictDi, serviceName) 。這裏的第一個參數 fn 就是以前提到的能夠是數組也能夠是函數。你們本身去看 annotate 的定義吧,就是這一句,提取出了全部依賴的名字,對於隱式注入試用 toString 加上 正則匹配來提取的,因此若是 angular 應用代碼壓縮時進行了變量名混淆的話,隱式注入就失效了。繼續看,提取出名字以後,經過 getService 獲取到了每個依賴的實例,最後在用 fn.apply 傳入依賴便可。 還記得以前的 providerInjector 嗎,它實際上是用來提供一些快速註冊 service 等可依賴實例的。它提供的一些方法其實都直接暴露到了 angular 對象上,你們若是仔細看過文檔其實就很明瞭了:
整體來講依賴注入在實現上並無什麼特別巧妙的地方,但有價值的是angular從很早就有了完整的模塊化體系,依賴是模塊化體系中很重要的一部分。而模塊化的意義也不僅是拆分、解耦而已,從工程實踐的角度來講,模塊化是實現那些超越單個工程師所能掌握的大工程的基石之一。
視圖編譯
關於 $compile 的使用和相應地內部機制其實文檔已經很詳細了。看這裏。咱們這裏看源碼的目的有兩個:一是看數據改動時觸發的 $digest 具體是如何更新視圖的;二是看源碼是否有些精妙之處能夠學習。 打開 /src/ng/compile.js 511行,注意到這裏定義的 $compileProvider 是 provider 的寫法,不熟悉的請去看下文檔。provider在用的時候會實例化,而咱們在用的 $compile 函數實際上就是 this.$get 這個數組的最後一個元素(一個函數)的返回值。跳到638行看定義,源碼太長,我就不貼了。後面只貼關鍵的地方。這個函數的返回了一個叫compile的函數:
function compile($compileNodes, transcludeFn, maxPriority, ignoreDirective, previousCompileContext) { /*省略若干行預處理節點的代碼*/ var compositeLinkFn = compileNodes($compileNodes, transcludeFn, $compileNodes, maxPriority, ignoreDirective, previousCompileContext); safeAddClass($compileNodes, 'ng-scope'); return function publicLinkFn(scope, cloneConnectFn, transcludeControllers){ /*省略若干行和cloneConnectFn等有關的代碼*/ if (compositeLinkFn) compositeLinkFn(scope, $linkNode, $linkNode); return $linkNode; }; }
沒有什麼神奇的,返回的這個publicLinkFn就是咱們用來link scope的函數。而這個函數實際上又是調用了 compileNodes 生成的 compositeLinkFn。若是你熟悉 directive 的使用,那咱們不妨輕鬆地猜想一下這個 compileNodes 應該就是收集了節點中的各類指令而後調用相應地compile函數,並將link函數組合起來成爲一個新函數,也就是這個compositeLinkFn以供調用。而 directive 裏的link函數扮演了將scope的變化映射到節點上(使用 scope.$watch),將節點變化映射到scope(一般要用scope.$apply來觸發scope.$digest)的角色。 我能夠直接說「恭喜你,猜對了」嗎?這裏沒什麼複雜的,你們本身看下吧。值得再看看的是scope.$watch 和 scope.$digest。一般咱們用 watch 來將視圖更新函數註冊相應地scope下,用digest來對比當前scope的屬性是否有變更,若是有變化就調用註冊的這些函數。我前面文章中說的angular性能不如ko等框架而且可能遇到瓶頸就是出於這個機制。咱們來翻一下$digest的底:
$digest: function() { /*省略若干變量定義代碼*/ beginPhase('$digest'); lastDirtyWatch = null; do { // "while dirty" loop dirty = false; current = target; /*省略若干行異步任務代碼*/ 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); /*省略若干行log代碼*/ } 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 error*/ } } while (dirty || asyncQueue.length); clearPhase(); while(postDigestQueue.length) { try { postDigestQueue.shift()(); } catch (e) { $exceptionHandler(e); } } }
這段代碼有兩個關鍵的loop,對應兩個關鍵概念。大loop就是所謂的dirty check。什麼是dirty?只要進入了這個循環,就是dirty的,直到值已經穩定下來。咱們看到源碼中用了lastDirtyWatch來做爲標記,要使watch === lastDirtyWatch,至少第二次循環才能實現。這是由於在調用監聽函數的時候,監聽函數自己可能去修改屬性,因此咱們必須等到值已經徹底不變了(或者超過了最大循環值)才能結束digest。另外看那個insanity warning,digest是進行深度優先遍歷檢測的。因此在設計複雜的directive時,要很是注意在scope哪一個層級調用digest。在寫簡單應用的時候,dirty check和遍歷子元素都沒有什麼問題,可是相比於基於observer的模式,最主要的缺點是它的全部監聽函數都是註冊在scope上的,每次digest都要檢測全部的watcher是否有變化。
最後總結一下視圖,angular在視圖層的設計上較爲完備,但同時概念也更多更復雜,在首屏渲染時速度不夠快。而且內存開銷是vue ko等輕框架倍數級的。但它的自己的規範和各個方面考慮的周全性確是很是值得學習,實際上也對後來者產生了極大的指導性意義。
其餘
這裏再記錄一個實踐中的問題,就是如何對數據實現getter 和setter?好比說這樣一個場景:有個三個輸入框,第一個讓用戶填姓,第二個填名,第三個自動顯示「姓+空格+名」。用戶也能夠直接在第三個框中填,第一框和第二框會自動變化。這個時候若是有相似於ko的computed property就簡單了,否則只能用$watch加中間變量去實現,代碼會有點難看。有代碼潔癖的話相信各位早晚會碰到這個問題,如下提供幾個參考資料:
總結
整體來講,AngularJS不管在設計仍是實踐上都具備指導性意義。對新手來講學習曲線較陡,但若是能深刻,收穫是很大的。AngularJS自己在工程上也有不少其餘產出,好比karma,從它中間獨立出來發展成了通用測試框架。仍是建議各位讀者能夠跟一跟AngularJS2.0的開發,必能受益。