Angular.js源码分析之开篇 执行流程

从本篇开始主要来分析下angular.js(v1.4.7)的源码,这是第一篇,先看最基础的,整个angular的初始化过程是什么样的。

结构

首先看代码最后有这样的代码:

// 真正的开始执行部分
//try to bind to jquery now so that one can write jqLite(document).ready()
//but we will rebind on bootstrap again.
bindJQuery();

publishExternalAPI(angular);

angular.module("ngLocale", [], ["$provide", function($provide) {
// 代码
}]);

jqLite(document).ready(function() {
  angularInit(document, bootstrap);
});

这个过程很明显,也就是首先执行bindJQuery,然后再调用publishExternalAPI,在页面load之后初始化angular,也就是angularInit(document, bootstrap)。所以说源码分析也会按照这个顺序进行。

bindJQuery

从名字就可以看出,主要是绑定jQuery的,这是因为在angular中还有一个实现了部分jQuery功能的JQLite,如果判断了jQuery是存在的,那么angular.element就直接赋值为jQuery了,否则就为angular自身实现的JQLite,下边看一下带分析的代码:

var bindJQueryFired = false;
var skipDestroyOnNextJQueryCleanData;
// 绑定angular.element
// 如果有jquery就直接用jquery
// 否则就是JQLite
function bindJQuery() {
  var originalCleanData;

  if (bindJQueryFired) {
    return;
  }

  // bind to jQuery if present;
  var jqName = jq();
  jQuery = isUndefined(jqName) ? window.jQuery :   // use jQuery (if present)
           !jqName             ? undefined     :   // use jqLite
                                 window[jqName];   // use jQuery specified by `ngJq`

  // Use jQuery if it exists with proper functionality, otherwise default to us.
  // Angular 1.2+ requires jQuery 1.7+ for on()/off() support.
  // Angular 1.3+ technically requires at least jQuery 2.1+ but it may work with older
  // versions. It will not work for sure with jQuery <1.7, though.
  if (jQuery && jQuery.fn.on) {
    jqLite = jQuery;
    // 扩展jquery的原型
    // 加入这些方法
    extend(jQuery.fn, {
      scope: JQLitePrototype.scope,
      isolateScope: JQLitePrototype.isolateScope,
      controller: JQLitePrototype.controller,
      injector: JQLitePrototype.injector,
      inheritedData: JQLitePrototype.inheritedData
    });

    // All nodes removed from the DOM via various jQuery APIs like .remove()
    // are passed through jQuery.cleanData. Monkey-patch this method to fire
    // the $destroy event on all removed nodes.
    originalCleanData = jQuery.cleanData;
    // 为了加入destroy事件 重写
    jQuery.cleanData = function(elems) {
      var events;
      if (!skipDestroyOnNextJQueryCleanData) {
        for (var i = 0, elem; (elem = elems[i]) != null; i++) {
          events = jQuery._data(elem, "events");
          if (events && events.$destroy) {
            jQuery(elem).triggerHandler('$destroy');
          }
        }
      } else {
        skipDestroyOnNextJQueryCleanData = false;
      }
      originalCleanData(elems);
    };
  } else {
    jqLite = JQLite;
  }

  angular.element = jqLite;

  // Prevent double-proxying.
  bindJQueryFired = true;
}

从上边的代码能直接看出来,基本上是用了jQuery的了,额外的加入了一些方法,这些方法直接从JQLitePrototype上直接获取了;那么下边就直接看下这几个方法的具体实现:

function jqLiteController(element, name) {
  return jqLiteInheritedData(element, '$' + (name || 'ngController') + 'Controller');
}
function jqLiteInheritedData(element, name, value) {
  // if element is the document object work with the html element instead
  // this makes $(document).scope() possible
  if (element.nodeType == NODE_TYPE_DOCUMENT) {
    element = element.documentElement;
  }
  var names = isArray(name) ? name : [name];

  while (element) {
  	// 意思就是一直找parentNode,直至没有
    for (var i = 0, ii = names.length; i < ii; i++) {
    	// 首先判断有没有这个name的数据 有的话 就找到了 返回
      if (isDefined(value = jqLite.data(element, names[i]))) return value;
    }

    // If dealing with a document fragment node with a host element, and no parent, use the host
    // element as the parent. This enables directives within a Shadow DOM or polyfilled Shadow DOM
    // to lookup parent controllers.
    element = element.parentNode || (element.nodeType === NODE_TYPE_DOCUMENT_FRAGMENT && element.host);
  }
}
// 省略代码
forEach({
  data: jqLiteData,
  inheritedData: jqLiteInheritedData,

  // 主要关注下这几个和jquery不同的
  // 方法就可以了
  // 其实主要还是在compile或者link的时候
  // 给元素设置了data
  // 所以可以直接获取了
  scope: function(element) {
    // Can't use jqLiteData here directly so we stay compatible with jQuery!
    return jqLite.data(element, '$scope') || jqLiteInheritedData(element.parentNode || element, ['$isolateScope', '$scope']);
  },

  isolateScope: function(element) {
    // Can't use jqLiteData here directly so we stay compatible with jQuery!
    return jqLite.data(element, '$isolateScope') || jqLite.data(element, '$isolateScopeNoTemplate');
  },

  controller: jqLiteController,

  injector: function(element) {
    return jqLiteInheritedData(element, '$injector');
  }
  // 省略代码
}, function(fn, name) {
  /**
   * Properties: writes return selection, reads return first value
   */
  JQLite.prototype[name] = function(arg1, arg2) {
    var i, key;
    var nodeCount = this.length;

    // jqLiteHasClass has only two arguments, but is a getter-only fn, so we need to special-case it
    // in a way that survives minification.
    // jqLiteEmpty takes no arguments but is a setter.
    if (fn !== jqLiteEmpty &&
        (isUndefined((fn.length == 2 && (fn !== jqLiteHasClass && fn !== jqLiteController)) ? arg1 : arg2))) {
      if (isObject(arg1)) {

        // we are a write, but the object properties are the key/values
        for (i = 0; i < nodeCount; i++) {
          if (fn === jqLiteData) {
            // data() takes the whole object in jQuery
            fn(this[i], arg1);
          } else {
            for (key in arg1) {
              fn(this[i], key, arg1[key]);
            }
          }
        }
        // return self for chaining
        return this;
      } else {
        // we are a read, so read the first child.
        // TODO: do we still need this?
        var value = fn.$dv;
        // Only if we have $dv do we iterate over all, otherwise it is just the first element.
        var jj = (isUndefined(value)) ? Math.min(nodeCount, 1) : nodeCount;
        for (var j = 0; j < jj; j++) {
          var nodeValue = fn(this[j], arg1, arg2);
          value = value ? value + nodeValue : nodeValue;
        }
        return value;
      }
    } else {
      // we are a write, so apply to all children
      for (i = 0; i < nodeCount; i++) {
        fn(this[i], arg1, arg2);
      }
      // return self for chaining
      return this;
    }
  };
});

其实基本上都是直接读取的和元素相关联的data数据,直接处理返回的。这是因为在angular的compile以及link阶段会给元素挂载一些必要的data数据。

publishExternalAPI

接着就是要说这个函数了,先直接看代码:

// 暴露API
function publishExternalAPI(angular) {
	// 暴露常用api
  extend(angular, {
    'bootstrap': bootstrap,
    'copy': copy,
    // 省略
  });

  // 定义返回angular.module
  angularModule = setupModuleLoader(window);
  // 定义内部的核心ng模块
  // 但是依赖ngLocale模块
  angularModule('ng', ['ngLocale'], ['$provide',
    function ngModule($provide) {
      // 省略
  ]);
}

从代码可以清楚地看出他干的事情

  • 将一些基础方法暴露到angular上

  • 通过setupModuleLoader定义返回angular.module

  • 定义angular的核心模块ng

下边首先来看看setupModuleLoader这个这个方法有神马玄虚:

// 主要定义angular.module
function setupModuleLoader(window) {

  var $injectorMinErr = minErr('$injector');
  var ngMinErr = minErr('ng');

  // 如果obj对象有name的值就返回 否则
  // 调用factory并复制obj[name],返回
  // 从名字也可以看出 主要是为了“确保”
  // obj[name]只会被初始化一次
  function ensure(obj, name, factory) {
    return obj[name] || (obj[name] = factory());
  }

  // 确保window.angular是一个对象
  var angular = ensure(window, 'angular', Object);

  // We need to expose `angular.$$minErr` to modules such as `ngResource` that reference it during bootstrap
  angular.$$minErr = angular.$$minErr || minErr;


  return ensure(angular, 'module', function() {
    /** @type {Object.<string, angular.Module>} */
    // 保存所有的modules
    var modules = {};

    // 返回的确保的唯一一个angular.module定义函数
    return function module(name, requires, configFn) {
      var assertNotHasOwnProperty = function(name, context) {
        if (name === 'hasOwnProperty') {
          throw ngMinErr('badname', 'hasOwnProperty is not a valid {0} name', context);
        }
      };
      // 模块名当然不能是hasOwnProperty
      assertNotHasOwnProperty(name, 'module');
      if (requires && modules.hasOwnProperty(name)) {
        modules[name] = null;
      }
      // 保证相同name的module只有一个实例对象
      return ensure(modules, name, function() {
        // 定义具体代码 留到下边分析
      });
    };
  });
}

大部分的逻辑已经在代码中注解了,主要作用也就是暴露angular.module方法,调用该方法的时候返回一个moduleInstance实例,至于详细的执行逻辑请看下边创建ng核心module的分析。

publishExternalAPI中调用完setupModuleLoader之后紧接着就是调用angularModule('ng',)其实也就是angular.module方法,传入了一个数组(angular的依赖注入),看详细代码:

angularModule('ng', ['ngLocale'], ['$provide',
  function ngModule($provide) {
    // $$sanitizeUriProvider needs to be before $compileProvider as it is used by it.
    $provide.provider({
      $$sanitizeUri: $$SanitizeUriProvider
    });
    // 创建$compile的$CompileProvider
    $provide.provider('$compile', $CompileProvider).
    // 以及基本上所有的内置指令
      directive({
          a: htmlAnchorDirective,
          input: inputDirective,
          // 省略
          ngBind: ngBindDirective,
          ngBindHtml: ngBindHtmlDirective,
          // 省略
      }).
      directive({
        ngInclude: ngIncludeFillContentDirective
      }).
      directive(ngAttributeAliasDirectives).
      directive(ngEventDirectives);
    // 继续一堆的provider
    $provide.provider({
      $anchorScroll: $AnchorScrollProvider,
      // 省略
      $controller: $ControllerProvider,
      // 省略
      $rootScope: $RootScopeProvider,
      // 省略
    });
  }
]);

有上边setupModuleLoader的分析,咱们知道定义的ng模块依赖ngLocale模块,但是现在还没有定义这个模块呢,直接依赖不会报错吗?这里就要看看angular.module到底是具体怎么处理的了:

// 在setupModuleLoader中返回的定义module的方法
return ensure(modules, name, function() {
  if (!requires) {
    // 参数不合法
    throw $injectorMinErr('nomod', "Module '{0}' is not available! You either misspelled " +
       "the module name or forgot to load it. If registering a module ensure that you " +
       "specify the dependencies as the second argument.", name);
  }

  /** @type {!Array.<Array.<*>>} */
  // invoke队列
  var invokeQueue = [];

  // 下边的这两个是因为
  // angular.module的返回值有config和run函数
  // 其实就是为了加入provider(service)用于注入用
  /** @type {!Array.<Function>} */
  // 配置块
  var configBlocks = [];
  /** @type {!Array.<Function>} */
  // run块
  var runBlocks = [];
	
	// 晚会的逻辑就是先存到数组队列中
	// 等到需要的时候(loadModules)再执行
  // 晚会再调用$injector.invoke,加入到configBlocks中
  // 因为要执行configBlocks中的东西的话,通过调用$injector.invoke才会执行
  var config = invokeLater('$injector', 'invoke', 'push', configBlocks);

  /** @type {angular.Module} */
  // 模块实例
  var moduleInstance = {
    // Private state
    _invokeQueue: invokeQueue,
    _configBlocks: configBlocks,
    _runBlocks: runBlocks,

    // 依赖模块们
    requires: requires,

    name: name,

    // invokeLaterAndSetModuleName和invokeLater差不多
    // 只是会设置下factoryFunction的$$moduleName为当然模块的name
    // 加上$$moduleName就知道报错的时候是那个模块报错了
    // 其实到这里也会发现其实调用module实例的provider、factory等暴露的
    // 方法的时候并不会立即执行配置函数,只是往module实例的configBlocks
    // 或者invokeQueue中push或者unshift了新的数组(配置数组,包含
    // 注入的东西以及要执行的对应的function)
    // 
    // 而且module实例暴露的这些便利方法其实都是$provide(或$animateProvider等)的对应方法
    // 所以真正的实现还是在对应的$provide中
    provider: invokeLaterAndSetModuleName('$provide', 'provider'),

    factory: invokeLaterAndSetModuleName('$provide', 'factory'),

    service: invokeLaterAndSetModuleName('$provide', 'service'),

    value: invokeLater('$provide', 'value'),
    // 注意这里是怎么实现常量效果的
    // 通过给 invokeQueue unshift
    // $provide的constant方法
    // 所以是一个逆序的过程
    // [后边设定的值,最开始设定的值]
    // 所以说执行完之后 就是得到的 最开始设定的那个值
    constant: invokeLater('$provide', 'constant', 'unshift'),

    decorator: invokeLaterAndSetModuleName('$provide', 'decorator'),

    animation: invokeLaterAndSetModuleName('$animateProvider', 'register'),

    filter: invokeLaterAndSetModuleName('$filterProvider', 'register'),

    controller: invokeLaterAndSetModuleName('$controllerProvider', 'register'),

    directive: invokeLaterAndSetModuleName('$compileProvider', 'directive'),

    config: config,

    run: function(block) {
      runBlocks.push(block);
      return this;
    }
  };

  if (configFn) {
  	// 把configFn插入到configBlocks中
    config(configFn);
  }
  return moduleInstance;

  function invokeLater(provider, method, insertMethod, queue) {
    if (!queue) queue = invokeQueue;
    return function() {
      queue[insertMethod || 'push']([provider, method, arguments]);
      return moduleInstance;
    };
  }
  function invokeLaterAndSetModuleName(provider, method) {
    return function(recipeName, factoryFunction) {
      if (factoryFunction && isFunction(factoryFunction)) factoryFunction.$$moduleName = name;
      invokeQueue.push([provider, method, arguments]);
      return moduleInstance;
    };
  }
});

所以就拿核心ng的定义来说,执行完成之后的模块实例其实就是这样子的:

{
  // Private state
  _invokeQueue: [],
  _configBlocks: [
    '$injector',
    'invoke',
    ['$provide', function ngModule($provide) {/***/}]
  ],
  _runBlocks: [],

  // 依赖模块们
  requires: ['ngLocale'],

  name: 'ng',

  // 一堆方法

  run: function(block) {
    runBlocks.push(block);
    return this;
  }
}

所以说可以看到每定义一个module其实也就是往_invokeQueue, _configBlocks, _runBlocks三个数组中增加你的代码块而已,此时并不会执行的。

OK,截止到这里基本上关于publishExternalAPI的分析基本完成了

ngLocale

紧着定义了ngLocale模块,具体细节一样,现在是不会执行的,结构:

angular.module("ngLocale", [], ["$provide", function($provide) {
// 代码
}]);

angularInit

最后的部分是这样的代码:

jqLite(document).ready(function() {
  angularInit(document, bootstrap);
});

可以看到在页面ready之后就执行了angularInit,下边就看看他是如何执行的把。

var ngAttrPrefixes = ['ng-', 'data-ng-', 'ng:', 'x-ng-'];
/**
 * angular初始化封装
 * element就是要初始化app的container或者元素本身
 * bootstrap就是初始化函数
 */
function angularInit(element, bootstrap) {
  var appElement,
      module,
      config = {};

  // The element `element` has priority over any other element
  forEach(ngAttrPrefixes, function(prefix) {
    var name = prefix + 'app';

    if (!appElement && element.hasAttribute && element.hasAttribute(name)) {
      appElement = element;
      module = element.getAttribute(name);
    }
  });
  forEach(ngAttrPrefixes, function(prefix) {
    var name = prefix + 'app';
    var candidate;

    if (!appElement && (candidate = element.querySelector('[' + name.replace(':', '\\:') + ']'))) {
      appElement = candidate;
      module = candidate.getAttribute(name);
    }
  });
  if (appElement) {
    config.strictDi = getNgAttribute(appElement, "strict-di") !== null;
    bootstrap(appElement, module ? [module] : [], config);
  }
}

简单说下,也就是查找页面上带有类似于ng-app="xx"这样的元素,然后以该元素为根元素初始化,调用bootstrap。下边就该bootstrap闪亮登场了:

/**
 * 初始化函数
 * @param  element 初始化app的根元素
 * @param  modules 依赖那些模块的名字集合
 * @param  config  额外配置
 */
function bootstrap(element, modules, config) {
  if (!isObject(config)) config = {};
  // 默认配置
  // strictDi 如果设置为true
  // 那么就意味着可以在压缩文件中找到Bug信息
  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);
      //Encode angle brackets to prevent input from being sanitized to empty string #8683
      throw ngMinErr(
          'btstrpd',
          "App Already Bootstrapped with this Element '{0}'",
          tag.replace(/</,'&lt;').replace(/>/,'&gt;'));
    }

    modules = modules || [];
    // 注入$provide
    // 同时设置$rootElement
    modules.unshift(['$provide', function($provide) {
      $provide.value('$rootElement', element);
    }]);

    if (config.debugInfoEnabled) {
      // Pushing so that this overrides `debugInfoEnabled` setting defined in user's `modules`.
      // 复写debugInfoEnabled配置
      modules.push(['$compileProvider', function($compileProvider) {
        $compileProvider.debugInfoEnabled(true);
      }]);
    }
    // 注入核心ng模块
    modules.unshift('ng');
    // 创建注入器
    // 隐藏了注入机制
    // 详见createInjector函数
    var injector = createInjector(modules, config.strictDi);
    // 注入这几个服务执行bootstrapApply
    injector.invoke(['$rootScope', '$rootElement', '$compile', '$injector',
       function bootstrapApply(scope, element, compile, injector) {
        // 省略
      }]
    );
    return injector;
  };

  var NG_ENABLE_DEBUG_INFO = /^NG_ENABLE_DEBUG_INFO!/;
  var NG_DEFER_BOOTSTRAP = /^NG_DEFER_BOOTSTRAP!/;

  if (window && NG_ENABLE_DEBUG_INFO.test(window.name)) {
    // 调试信息
    config.debugInfoEnabled = true;
    window.name = window.name.replace(NG_ENABLE_DEBUG_INFO, '');
  }

  // 非延迟初始化
  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);
    });
    return doBootstrap();
  };

  if (isFunction(angular.resumeDeferredBootstrap)) {
    angular.resumeDeferredBootstrap();
  }
}

看真正的初始化执行逻辑也就是在doBootstrap中,其中他强行注入了ng核心模块,并且设置了rootElement,这里就很明显看到注入器的影子了,本篇就不对注入器做详细分析了,留到下篇来分析。

结语

从上边的分析,可以清楚的知道angular在初始化的时候是要进行哪些步骤的,当然还有很关键的该怎么执行代码,分析编译模板,绑定啊等等的操作。本篇只是说下初始化的流程问题,剩余的留到下篇(下下篇)分析。

欢迎吐槽!

发布于: 2015年 10月 21日