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的技術點。因為本人也是邊看邊跟蹤流程,有不對的地方望大牛指出。