當你用AngularJS寫的應用越多, 你會愈加的以爲它至關神奇. 以前我用AngularJS實現了至關多酷炫的效果, 因此我決定去看看它的源碼, 我想這樣也許我能知道它的原理. 下面是我從源碼中找到的一些能夠了解AngularJS那些高級(和隱藏)功能如何實現的代碼.正則表達式
依賴注入(DI)讓咱們能夠不用本身實例化就能建立依賴對象的方法. 簡單的來講, 依賴是以注入的方式傳遞的. 在Web應用中, Angular讓咱們能夠經過DI來建立像Controllers和Directives這樣的對象. 咱們還能夠建立本身的依賴對象, 當咱們要實例化它們時, Angular能自動實現注入.數組
最多見的被注入對象應該是 $scope
對象. 它能夠像下面這樣被注入的:瀏覽器
function MainCtrl ($scope) { // access to $scope } angular .module(‘app’) .controller(‘MainCtrl’, MainCtrl);
對於歷來沒有接觸過依賴注入的Javascript開發人員來講, 這樣看起來只是像傳遞了一個參數. 而實際上, 他是一個依賴注入的佔位符. Angular經過這些佔位符, 把真正的對象實例化給咱們, 讓來看看他是怎麼實現的.安全
當你運行你代碼的時候, 若是你把function聲明中的參數換成一個其它字母, 那麼Angular就沒法找到你真正想實例化的對象. 由於Angular在咱們的function上使用了 toString()
方法, 他將把咱們的整個function變成一個字符串, 而後解析function中聲明的每個參數. 它使用下面4個正則(RegExps)來完成這件事情.閉包
var FN_ARGS = /^function\s*[^\(]*\(\s*([^\)]*)\)/m; var FN_ARG_SPLIT = /,/; var FN_ARG = /^\s*(_?)(\S+?)\1\s*$/; var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg;
Angular作的第一件事情就是把咱們的整個function轉換爲字符串, 這確實是Javascript很強大的地方. 轉換後咱們將獲得以下字符串:app
‘function MainCtrl ($scope) {...}’
而後, 他用正則移除了在 function()
中有可能的全部的註釋.框架
fnText = fn.toString().replace(STRIP_COMMENTS, '');
接着它提取其中的參數部分.async
argDecl = fnText.match(FN_ARGS);
最後它使用 .split()
方法來移除參數中的全部空格, 完美! Angular使用一個內部的 forEach
方法來遍歷這些參數, 而後把他們放入一個 $inject
數組中.ide
forEach(argDecl[1].split(FN_ARG_SPLIT), function(arg) { arg.replace(FN_ARG, function(all, underscore, name) { $inject.push(name); }); });
正如你如今想的, 這是一個很大的性能開銷操做. 每一個函數都要執行4個正則表達式還有大量的轉換操做----這將給咱們帶來性能損失. 不過咱們能夠經過直接添加須要注入的對象到 $inject
數組中的方式來避免這個開銷.函數
咱們能夠在function對象上添加一個 $inject
屬性來告訴Angular咱們的依賴對象. 若是對象是存在的, Angular將實例化它. 這樣的語法更具備可讀性, 由於咱們能夠這些對象是被注入的. 下面是一個例子:
function SomeCtrl ($scope) { } SomeCtrl.$inject = ['$scope']; angular .module('app', []) .controller('SomeCtrl', ['$scope', SomeCtrl]);
這將節省框架的大量操做, 它不用再解析function的參數, 也不用去操做數組(查看下一節數組參數), 它能夠直接獲取咱們已經傳遞給他的 $inject
屬性. 簡單, 高效.
理想狀況下咱們應該使用構建工具, 好比 Grunt.js
或者 Gulp.js
來作這個事情: 讓他們在編譯時生成相應的 $injext
屬性, 這樣能讓Web應用運行的更快.
注: 實際上上面介紹的內容並不涉如何實例化那些須要被注入的對象. 整個操做只是標記出須要的名字----實例化的操做將由框架的另外一部分來完成.
最後要提到的是數組參數. 數組的前面每一個元素的名字和順序, 剛是數組最後一個元素function的參數名字和順序. 好比: [‘$scope’, function ($scope) {}]
.
這個順序是很是重要的, 由於Angular是以這個順序來實例化對象. 若是順序不正確, 那麼它可能將其它對象錯誤的實例化到你真正須要的對象上.
function SomeCtrl ($scope, $rootScope) { } angular .module('app', []) .controller('SomeCtrl', ['$scope', ‘$rootScope’, SomeCtrl]);
像上面同樣, 咱們須要作的就是把函數最爲數組的最後一個元素. 而後Angular會遍歷前面的每個元素, 把它們添加到 $inject
數組中. 當Angular開始解析一個函數的時候, 它會先檢查目標對象是否是一個數組類型, 若是是的話, 他將把最後一個元素做爲真正的function, 其它的元素都做爲依賴對象添加到 $inject
中.
} else if (isArray(fn)) { last = fn.length - 1; assertArgFn(fn[last], 'fn'); $inject = fn.slice(0, last); }
Factory和Service看起來很是類似, 以致於不少開發人員都沒法理解它們有什麼不一樣.
當實例化一個 .service()
的時候, 其實他將經過調用 new Service()
的形式來給咱們建立一個新的實例, .service()
的方法像是一個構造函數.
服務(service)實際上來講是一個最基本的工廠(factory), 可是它是經過 new
來建立的, 你須要使用 this
來添加你須要的變量和函數, 最後返回這個對象.
工廠(factory)其實是很是接近面向對象中的"工廠模式(factory pattern)". 當你調用時, 它會建立新的實例. 本質上來講, 那個實例是一個全新的對象.
下面是Angular內部實際執行的源碼:
function factory(name, factoryFn) { return provider(name, { $get: factoryFn }); } function service(name, constructor) { return factory(name, ['$injector', function($injector) { return $injector.instantiate(constructor); }]); }
全部的scope對象都繼承於 $rootScope
, $rootScope
又是經過 new Scope()
來建立的. 全部的子scope都是用過調用 $scope.$new()
來建立的.
var $rootScope = new Scope();
它內部有一個 $new
方法, 讓新的scope能夠從原型鏈上引用它們的父scope, 子scope(爲了digest cycle), 以及先後的scope.
從下面的代碼能夠看出, 若是你想建立一個獨立的scope, 那麼你應該使用 new Scope()
, 不然它將以繼承的方式來建立.
我省略了一些沒必要要的代碼, 下面是他的核心實現
$new: function(isolate) { var child; if (isolate) { child = new Scope(); child.$root = this.$root; } else { // Only create a child scope class if somebody asks for one, // but cache it to allow the VM to optimize lookups. if (!this.$$ChildScope) { this.$$ChildScope = function ChildScope() { this.$$watchers = null; }; this.$$ChildScope.prototype = this; } child = new this.$$ChildScope(); } child['this'] = child; child.$parent = this; return child; }
理解這一點對寫測試很是重要, 若是你想測試你的Controller, 那麼你應該使用 $scope.$new()
來建立$scope對象. 明白scope是如何建立的在測試驅動開發(TDD)中是十分重要的, 這將更加有助於你mock module.
digest cycle的實現其實就是咱們常常看到的 $digest
關鍵字, Angular強大的雙向綁定功能依賴於它. 每當一個model被更新時他都會運行, 檢查當前值, 若是和之前的不一樣, 將觸發listener. 這些都是髒檢查(dirty checking)的基礎內容. 他會檢查全部的model, 與它們原來的值進行比較, 若是不一樣, 觸發listener, 循環, 直到不在有變化爲止.
$scope.name = 'Todd'; $scope.$watch(function() { return $scope.name; }, function (newValue, oldValue) { console.log('$scope.name was updated!'); } );
當你調用 $scope.$watch
的時候, 實際上幹了2件事情. watch的第一個參數是一個function, 這個function的返回你想監控的對象(若是你傳遞的是一個string, Angular會把他轉換爲一個function). digest cycle 運行的時候, 它會調用這個function. 第二個參數也是一個function, 當第一個function的值發生變化的時候它會被調用. 讓咱們看看他是怎麼實現監控的:
$watch: function(watchExp, listener, objectEquality) { var get = $parse(watchExp); if (get.$$watchDelegate) { return get.$$watchDelegate(this, listener, objectEquality, get); } var scope = this, array = scope.$$watchers, watcher = { fn: listener, last: initWatchVal, get: get, exp: watchExp, eq: !!objectEquality }; lastDirtyWatch = null; if (!isFunction(listener)) { watcher.fn = noop; } if (!array) { array = scope.$$watchers = []; } // we use unshift since we use a while loop in $digest for speed. // the while loop reads in reverse order. array.unshift(watcher); return function deregisterWatch() { arrayRemove(array, watcher); lastDirtyWatch = null; }; }
這個方法將會把參數添加到scope中的 $$watchers
數組中, 而且它會返回一個function, 以便於你想結束這個監控操做.
而後digest cycle會在每次調用 $scope.$apply
或者 $scope.$digest
的時候運行. $scope.$apply
其實是一個rootScope的包裝, 他會從根$rootScope向下廣播. 而 $scope.$digest
只會在當前scope中運行(並向下級scope廣播).
$digest: function() { var watch, value, last, watchers, asyncQueue = this.$$asyncQueue, postDigestQueue = this.$$postDigestQueue, length, dirty, ttl = TTL, next, current, target = this, watchLog = [], logIdx, logMsg, asyncTask; 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, null) : value; watch.fn(value, ((last === initWatchVal) ? value : last), current); if (ttl < 5) { logIdx = 4 - ttl; if (!watchLog[logIdx]) watchLog[logIdx] = []; logMsg = (isFunction(watch.exp)) ? 'fn: ' + (watch.exp.name || watch.exp.toString()) : watch.exp; logMsg += '; newVal: ' + toJson(value) + '; oldVal: ' + toJson(last); watchLog[logIdx].push(logMsg); } } 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) { $exceptionHandler(e); } } } } while ((current = next)); // `break traverseScopesLoop;` takes us to here if((dirty || asyncQueue.length) && !(ttl--)) { clearPhase(); throw $rootScopeMinErr('infdig', '{0} $digest() iterations reached. Aborting!\n' + 'Watchers fired in the last 5 iterations: {1}', TTL, toJson(watchLog)); } } while (dirty || asyncQueue.length); clearPhase(); while(postDigestQueue.length) { try { postDigestQueue.shift()(); } catch (e) { $exceptionHandler(e); } } }
這個實現很是有才, 雖然我沒有進去看它是如何向下級廣播的, 但這裏的關鍵是循環遍歷 $$watchers
, 執行裏面的函數(就是那個你經過 $scope.$watch
註冊的第一個function), 而後若是獲得和以前不一樣的值, 他又將調用listener(那個你傳遞的第二個function). 而後, 砰! 咱們獲得了一個變量發生改變的通知. 關鍵是咱們是如何知道一個值發生變化了的? 當一個值被更新的時候digest cycle會運行(儘管它可能不是必須的). 好比在 ng-model
上, 每個keydown事件都會觸發digest cycle.
當你想在Angular框架以外作點什麼的時候, 好比在 setTimeout
的方法裏面你想讓Angular知道你可能改變了某個model的值. 那麼你須要使用 $scope.$apply
, 你把一個function放在它的參數之中, 那麼他會在Angular的做用域運行它, 而後在 $rootScope
上調用 $digest
. 它將向它下面全部的scope進行廣播, 這將觸發你註冊的全部listeners和watchers. 這一點意味着Angular能夠知道你更新了任何做用域的變量.
Angular實現polyfilling的方式很是巧妙, 它不是用像 Function.prototype.bind
同樣的方式直接綁定在一個對象的原型鏈上. Angular會調用一個function來斷定瀏覽器是否支持這個方法(基礎特徵檢查), 若是存在它會直接返回這個方法. 若是不存在, 他將使用一段簡短的代碼來實現它.
這樣是比較安全的方式. 若是直接在原型鏈上綁定方法, 那麼它可能會覆蓋其它類庫或者框架的代碼(甚至是咱們本身的代碼). 閉包也讓咱們能夠更安全的儲存和計算那些臨時變量, 若是存在這個方法, Angular將直接調用. 原生方法一般會帶來極大的性能提高.
Angular支持IE8+的瀏覽器(撰寫本文時Angular版本是1.2.x), 這意味着它仍是要兼容老的瀏覽器, 爲它們提供那些沒有的功能. 讓咱們來用 indexOf
來舉例.
function indexOf(array, obj) { if (array.indexOf) return array.indexOf(obj); for (var i = 0; i < array.length; i++) { if (obj === array[i]) return i; } return -1; }
它直接取代了原來的 array.indexOf
方法, 它本身實現了indexOf方法. 但若是瀏覽器支持這個函數, 他將直接調用原生方法. 十分簡單.
實現閉包能夠用一個當即執行函數(IIFE). 好比下面這個 isArray
方法, 若是瀏覽器不支持這個功能, 它將使用閉包返回一個 Array.isArray
的實現. 若是 Array.isArray
是一個函數, 那麼它將直接使用原生方法----又一個提升性能的方法. IIFE可讓咱們十分的方便來封裝一些東西, 而後只返回咱們須要的內容.
var isArray = (function() { if (!isFunction(Array.isArray)) { return function(value) { return toString.call(value) === '[object Array]'; }; } return Array.isArray; })();
這就是我看的第一部分Angular源碼, 第二部分將在下週發佈.