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的开发,必能受益。

相关文章
相关标签/搜索