MVVM大比拼之AngularJS源碼精析

MVVM大比拼之AngularJS源碼精析javascript

簡介html

AngularJS的學習資源已經很是很是多了,AngularJS基礎請直接看官網文檔。這裏推薦幾個深度學習的資料:vue

  • AngularJS學習筆記 做者:鄒業盛 。這個筆記很是細緻,記錄了做者對於AngularJS各個方面的思考,其中也不乏源碼級的分析。
  • 構建本身的AngularJS 。雖然放出第一章後做者就寫書去了。但這第一部分已經足以帶領讀者深刻窺探angularJS在覈心概念上的實現,特別是dirty check。有願意繼續深刻的讀者能夠去買書。
  • Design Decisions in AngularJS。 google io 上AngularJS做者的演講視頻,很是值得一看。

其實隨便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的開發,必能受益。

相關文章
相關標籤/搜索