我的angularjs源码学习之旅1——初识angularjs
angular诞生有好几年光景了,有Google公司的支持版本更新还是比较快,从一开始就是一个热门技术,但是本人近期才开始接触到。只能感慨自己学习起点有点晚了。只能是加倍努力赶上技术前线。
因为有分析jQuery源码学到很多东西的原因,所以本人对新技术还是抱有追根问底的习惯,希望能从本质上理解他们。前两天刚刚完成nodejs编写的一个小网站,给俺媳妇用的,所以就没有挂到外网上,只能本机启动自己用。开发完成后有点小收获小感悟就在这里唠叨几句。
第一个要唠叨的是关于抛异常。对前端来说,前端抛异常很多时候是不用去处理用户也感觉不出来的;而后端一抛异常如果没有异常处理机制,那就是整个程序直接挂掉了。从这个上面来说本人感觉后端的代码必须稳定、健壮,所以给个人的感觉是后端程序员更加严肃,而前端程序员更加的活泼,当然这里并不是说前端出异常就不去处理。
第二个是加密的问题。对于前端来说加密往往是后端的事,往往传输给后端的都是明文密码,本人对此也很难以理解,按说前端也应该加密才对,至少哪些个截获我们发送的信息的人需要一定代价才能破解我们的明文密码。但实际运用中往往都没有前端加密这个环节,尽管大部分网站都声称,不会存储用户的明文密码。但这并没有证据,也许私下里仍在悄悄储存。如果在前端加密,网站就无法拿到用户的明文密码了。也许正是这一点,很多网站不愿意使用前端加密。现在用nodejs了,那么前端人去做后端程序也应当对密码加密才对。关于加密可以参考这篇文章对抗拖库 —— Web 前端慢加密
第三个是和数据库打交道。对于简单的系统来说还好,至少不会花太多的时间去学习数据库查询,但是如果是比较大的系统的话,那么需要花更多的时间去学习和优化数据库查询了,这是一个很让前端人头疼的事。
第四是关于模板引擎的事。作为前端人员来说最不希望的是html代码中插入一些业务代码。比如nodejs比较推荐的ejs妥妥的jsp风格。本人是不赞成这种写法的,给后端开发人员用还可以。html就应该是纯html,没有任何业务逻辑,特别是下面这种情况:html代码和逻辑代码完全杂在一起了。
<% if (names.length) { %> <ul> <% names.forEach(function(name){ %> <li><%= name %></li> <% }) %> </ul> <% } %>
对比angular:有逻辑,但是逻辑只是标签的属性而已,给人的感觉与纯html一样,看着舒服很多。这也是前端人员比较能接受的方式。
<ul> <li ng-repeat="x in names">{{x}} {{lastname}} </ul>
好了,唠叨了半天。学习nodejs也就到一段落,毕竟本人也没想真的做全栈式工程师,专攻前端是本人的理想。
先前通过菜鸟教程学习了angular的基本知识(本人英语不太好,要是看英语教程那叫一个头大)。本人有几个好奇。
1.MVC/MVP/MVVM这三个东东到底是什么东西?本人一直都是一知半解
2.如下的代码,input是怎么和{{name}}联动的?框架是怎么保存Hello {{name}}的,必须要保存吧,不然我改变了input内容框架怎么知道去刷新h1。
<div ng-app=""> <p>名字 : <input type="text" ng-model="name"></p> <h1>Hello {{name}}</h1> </div>
3.如下的代码中,函数中怎么知道我是依赖的$scope,怎么实现的依赖注入?
<script> var app = angular.module('myApp', []); app.controller('myCtrl', function($scope) { $scope.firstName = "John"; $scope.lastName = "Doe"; }); </script>
最后就罗列出了一堆的名称:MVVM、自动化双向数据绑定、依赖注入、脏检测等等。
分析一个源码最先要跟踪的就是他的执行流程,这是第一步。我们的实例代码是
<div ng-app="myApp" ng-controller='myCtrl'> <input type="text" ng-model='name'/> <span style='width: 100px;height: 20px; margin-left: 300px;'>{{name}}</span> </div> <script> var app = angular.module('myApp', []); app.controller('myCtrl', function($scope) { $scope.name = 1; }); </script>
本人跟踪angular执行流程如下。
1.bindJQuery();尝试绑定jQuery,如果没有jQuery则使用内置的JQLite
2.publishExternalAPI(angular);初始化angular的各种外部api。可以看一下初始化之前的angular对象是
初始化完成以后是
angular = { $$csp: function(), $$minErr: minErr(module, ErrorConstructor), $interpolateMinErr: function(), bind: bind(self, fn), bootstrap: bootstrap(element, modules, config), callbacks: Object, copy: copy(source, destination, stackSource, stackDest), element: JQLite(element), equals: equals(o1, o2), extend: extend(dst), forEach: forEach(obj, iterator, context), fromJson: fromJson(json), getTestability: getTestability(rootElement), identity: identity($), injector: createInjector(modulesToLoad, strictDi), isArray: isArray(), isDate: isDate(value), isDefined: isDefined(value), isElement: isElement(node), isFunction: isFunction(value), isNumber: isNumber(value), isObject: isObject(value), isString: isString(value), isUndefined: isUndefined(value), lowercase: (string), merge: merge(dst), module: module(name, requires, configFn), noop: noop(), reloadWithDebugInfo: reloadWithDebugInfo(), toJson: toJson(obj, pretty), uppercase: (string), version: Object, }
可以看到给angular添加了很多方法和属性。
其中用到 angularModule = setupModuleLoader(window);是用来给angular上添加module方法(angular添加模块的函数)
这个module方法有一个外部变量var modules = {};这个变量的作用马上就能看到。
angularModule('ng', ['ngLocale'], ['$provide',function ngModule($provide) {...}]);
执行结果会得到以后将会得到(这里面这个modules即是angular.module函数的那个外部变量)
modules.ng = moduleInstance = { _invokeQueue: [], _configBlocks:[["$injector","invoke",["$provide",ngModule($provide)]]], _runBlocks: [], animation: funciton(recipeName, factoryFunction), config: function(), constant: function(), controller: function(recipeName, factoryFunction), decorator: function(recipeName, factoryFunction), directive: function(recipeName, factoryFunction), factory: function(recipeName, factoryFunction), filter: function(recipeName, factoryFunction), name: "ng", provider: function(recipeName, factoryFunction), requires: ["ngLocale"], run: function(block), service: function(recipeName, factoryFunction), value: function() }
拆解一下这个函数的内部执行步骤和结果:
1)先定义了三个数组invokeQueue = [];var configBlocks = [];var runBlocks = [];
顾名思义invokeQueue是执行队列;configBlocks是配置块,马上我们就会对这个配置块赋值; runBlocks是运行了的块。
2)执行var config = invokeLater('$injector', 'invoke', 'push', configBlocks);得到的config如下
3)对象moduleInstance初始化,初始化中主要调用了两个方法invokeLater和invokeLaterAndSetModuleName,结果为
其中config属性对应的函数就是第二步的config。
可以看到执行里面的函数大都是在往invokeQueue队列里面塞执行数据,每一个执行数据包括三个元素:provider/method/arguments。后面正真执行的时候调用方式是provider[method].apply(provider, arguments)。
run函数把block放入到runBlock中。里面有个requires属性,表示要依赖的模块,比如当前name为“ng”时requires为["ngLocale"]。
小点:moduleInstance的大多数方法属性最后又返回了moduleInstance对象,和jQuery类似,这样实现链式调用。
4)执行if (configFn) {config(configFn);}
结合第二步的config函数来看即把('$injector','invoke', ['$provide',function ngModule($provide) {...}])塞入到configBlocks中
5)返回处理后的moduleInstance对象,这个对象就是modules.ng
3.调用angular.module("ngLocale", [], ["$provide", function($provide) {...}])再次添加一个ngLocale模块。
先前modules只有一个ng模块,现在变成了两个模块。
4.最后是等待文档加载完成以后进行angular初始化,这里面的初始化主要是识别html中的指令、
jqLite(document).ready(function() { angularInit(document, bootstrap); });
angularInit中处理是比较简单的,查找符合格式的"ng-"/"data-ng-"/"ng:"/"x-ng-" + "app"标签,作为使用angular的的标志。平常我们都使用ng-app,
ng-app 指令用于告诉 AngularJS 应用当前这个元素是根元素。所有 AngularJS 应用都必须要要一个根元素。HTML 文档中只允许有一个 ng-app
指令,如果有多个 ng-app
指令,则只有第一个会被使用。
找到根元素,代入bootstrap中执行
//config中包含依赖注入是否是严格注入的标志;module是模块名称,也就是ng-app指定的名称,appElement是angular应用的根元素的DOM对象 bootstrap(appElement, module ? [module] : [], config);
比较重要的是bootstrap中调用
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(/</,'<').replace(/>/,'>')); } modules = modules || []; modules.unshift(['$provide', function($provide) { $provide.value('$rootElement', element); }]); if (config.debugInfoEnabled) { // Pushing so that this overrides `debugInfoEnabled` setting defined in user's `modules`. modules.push(['$compileProvider', function($compileProvider) { $compileProvider.debugInfoEnabled(true); }]); } modules.unshift('ng'); var injector = createInjector(modules, config.strictDi); injector.invoke(['$rootScope', '$rootElement', '$compile', '$injector', function bootstrapApply(scope, element, compile, injector) { scope.$apply(function() { element.data('$injector', injector); compile(element)(scope); }); }] ); return injector; };
在初始化注入函数createInjector之前,modules结构如下
正真最重要的函数:createInjector(初始化依赖注入)。createInjector需要单独拿出来说
5.createInjector
function createInjector(modulesToLoad, strictDi) { strictDi = (strictDi === true); var INSTANTIATING = {}, providerSuffix = 'Provider', path = [], loadedModules = new HashMap([], true), 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(serviceName, caller) { if (angular.isString(caller)) { path.push(caller); } throw $injectorMinErr('unpr', "Unknown provider: {0}", path.join(' <- ')); })), instanceCache = {}, instanceInjector = (instanceCache.$injector = createInternalInjector(instanceCache, function(serviceName, caller) { var provider = providerInjector.get(serviceName + providerSuffix, caller); return instanceInjector.invoke(provider.$get, provider, undefined, serviceName); })); forEach(loadModules(modulesToLoad), function(fn) { if (fn) instanceInjector.invoke(fn); }); return instanceInjector;
...
返回的是instanceInjector。里面重要的几个变量的关系是providerInjector = providerCache.$injector;instanceInjector = instanceCache.$injector。
而providerCache的结构是
instanceCache的结构是
我们的实例代码最终走到createInjector函数中的
forEach(loadModules(modulesToLoad), function(fn) { if (fn) instanceInjector.invoke(fn); });
此时modulesToLoad为 ["ng", [ "$provide",function ($provide){...}], "myApp"]。
function loadModules(modulesToLoad) { assertArg(isUndefined(modulesToLoad) || isArray(modulesToLoad), 'modulesToLoad', 'not an array'); var runBlocks = [], moduleFn; forEach(modulesToLoad, function(module) {
//loadedModules = new HashMap([], true),这是一个哈希存储结构,将modulesToLoad里面的元素都存到hash表中 if (loadedModules.get(module)) return; loadedModules.put(module, true); function runInvokeQueue(queue) { var i, ii; for (i = 0, ii = queue.length; i < ii; i++) { var invokeArgs = queue[i], provider = providerInjector.get(invokeArgs[0]); //这里便是之前说的provider[method].apply(provider, arguments)的调用 provider[invokeArgs[1]].apply(provider, invokeArgs[2]); } } try { if (isString(module)) {//当module为字符串 //angularModule即angular.module,调用后返回moduleInstance对象
moduleFn = angularModule(module); //把所有依赖模块的runBlocks都取出
runBlocks = runBlocks.concat(loadModules(moduleFn.requires)).concat(moduleFn._runBlocks);
//将执行西面的两个队列 runInvokeQueue(moduleFn._invokeQueue); runInvokeQueue(moduleFn._configBlocks); } else if (isFunction(module)) { runBlocks.push(providerInjector.invoke(module)); } else if (isArray(module)) {
//第二参数 [ "$provide",function ($provide){...}],invoke方法执行后将结果保存存到runBlocks runBlocks.push(providerInjector.invoke(module)); } else { assertArgFn(module, 'module'); } } catch (e) { ... } }); return runBlocks; }
还有一个比较重要的函数createInternalInjector,顾名思义即用来创建依赖注入的。下一章接着分析angular实现的依赖注入。
创建了依赖注入对象injector,接下来就马上用起来了
6.执行injector.invoke(['$rootScope', '$rootElement', '$compile', '$injector', function bootstrapApply(scope, element, compile, injector) {...}])。
这个函数执行实现了数据的脏检测,使数据双向绑定。后面会分析他的实现方式。
好了,通过这6步,页面初始化即完成。里面有很多细节无法分析到位,本人觉得也没必要细究,毕竟还不是angular的技术创新点,大体了解一些angular执行流程即可,后面的分析才会分析angular的技术点。因为本人也是边看边跟踪流程,有不对的地方望大牛指出。