當你用AngularJS寫(xiě)的應用越多, 你會(huì )越發(fā)的覺(jué)得它相當神奇. 之前我用AngularJS實(shí)現了相當多酷炫的效果, 所以我決定去看看它的源碼, 我想這樣也許我能知道它的原理. 下面是我從源碼中找到的一些可以了解AngularJS那些高級(和隱藏)功能如何實(shí)現的代碼.
依賴(lài)注入(DI)讓我們可以不用自己實(shí)例化就能創(chuàng )建依賴(lài)對象的方法. 簡(jiǎn)單的來(lái)說(shuō), 依賴(lài)是以注入的方式傳遞的. 在Web應用中, Angular讓我們可以通過(guò)DI來(lái)創(chuàng )建像Controllers和Directives這樣的對象. 我們還可以創(chuàng )建自己的依賴(lài)對象, 當我們要實(shí)例化它們時(shí), Angular能自動(dòng)實(shí)現注入.
最常見(jiàn)的被注入對象應該是 $scope 對象. 它可以像下面這樣被注入的:
function MainCtrl ($scope) { // access to $scope}angular .module(‘a(chǎn)pp’) .controller(‘MainCtrl’, MainCtrl);對于從來(lái)沒(méi)有接觸過(guò)依賴(lài)注入的Javascript開(kāi)發(fā)人員來(lái)說(shuō), 這樣看起來(lái)只是像傳遞了一個(gè)參數. 而實(shí)際上, 他是一個(gè)依賴(lài)注入的占位符. Angular通過(guò)這些占位符, 把真正的對象實(shí)例化給我們, 讓來(lái)看看他是怎么實(shí)現的.
當你運行你代碼的時(shí)候, 如果你把function聲明中的參數換成一個(gè)其它字母, 那么Angular就無(wú)法找到你真正想實(shí)例化的對象. 因為Angular在我們的function上使用了 toString() 方法, 他將把我們的整個(gè)function變成一個(gè)字符串, 然后解析function中聲明的每一個(gè)參數. 它使用下面4個(gè)正則(RegExps)來(lái)完成這件事情.
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做的第一件事情就是把我們的整個(gè)function轉換為字符串, 這確實(shí)是Javascript很強大的地方. 轉換后我們將得到如下字符串:
‘function MainCtrl ($scope) {...}’然后, 他用正則移除了在 function() 中有可能的所有的注釋.
fnText = fn.toString().replace(STRIP_COMMENTS, '');接著(zhù)它提取其中的參數部分.
argDecl = fnText.match(FN_ARGS);最后它使用 .split() 方法來(lái)移除參數中的所有空格, 完美! Angular使用一個(gè)內部的 forEach 方法來(lái)遍歷這些參數, 然后把他們放入一個(gè) $inject 數組中.
forEach(argDecl[1].split(FN_ARG_SPLIT), function(arg) { arg.replace(FN_ARG, function(all, underscore, name) { $inject.push(name); });});正如你現在想的, 這是一個(gè)很大的性能開(kāi)銷(xiāo)操作. 每個(gè)函數都要執行4個(gè)正則表達式還有大量的轉換操作----這將給我們帶來(lái)性能損失. 不過(guò)我們可以通過(guò)直接添加需要注入的對象到 $inject 數組中的方式來(lái)避免這個(gè)開(kāi)銷(xiāo).
我們可以在function對象上添加一個(gè) $inject 屬性來(lái)告訴Angular我們的依賴(lài)對象. 如果對象是存在的, Angular將實(shí)例化它. 這樣的語(yǔ)法更具有可讀性, 因為我們可以這些對象是被注入的. 下面是一個(gè)例子:
function SomeCtrl ($scope) {}SomeCtrl.$inject = ['$scope'];angular .module('app', []) .controller('SomeCtrl', ['$scope', SomeCtrl]);這將節省框架的大量操作, 它不用再解析function的參數, 也不用去操作數組(查看下一節數組參數), 它可以直接獲取我們已經(jīng)傳遞給他的 $inject 屬性. 簡(jiǎn)單, 高效.
理想情況下我們應該使用構建工具, 比如 Grunt.js 或者 Gulp.js 來(lái)做這個(gè)事情: 讓他們在編譯時(shí)生成相應的 $injext 屬性, 這樣能讓W(xué)eb應用運行的更快.
注: 實(shí)際上上面介紹的內容并不涉如何實(shí)例化那些需要被注入的對象. 整個(gè)操作只是標記出需要的名字----實(shí)例化的操作將由框架的另一部分來(lái)完成.
最后要提到的是數組參數. 數組的前面每個(gè)元素的名字和順序, 剛是數組最后一個(gè)元素function的參數名字和順序. 比如: [‘$scope’, function ($scope) {}].
這個(gè)順序是非常重要的, 因為Angular是以這個(gè)順序來(lái)實(shí)例化對象. 如果順序不正確, 那么它可能將其它對象錯誤的實(shí)例化到你真正需要的對象上.
function SomeCtrl ($scope, $rootScope) {}angular .module('app', []) .controller('SomeCtrl', ['$scope', ‘$rootScope’, SomeCtrl]);像上面一樣, 我們需要做的就是把函數最為數組的最后一個(gè)元素. 然后Angular會(huì )遍歷前面的每一個(gè)元素, 把它們添加到 $inject 數組中. 當Angular開(kāi)始解析一個(gè)函數的時(shí)候, 它會(huì )先檢查目標對象是不是一個(gè)數組類(lèi)型, 如果是的話(huà), 他將把最后一個(gè)元素作為真正的function, 其它的元素都作為依賴(lài)對象添加到 $inject 中.
} else if (isArray(fn)) { last = fn.length - 1; assertArgFn(fn[last], 'fn'); $inject = fn.slice(0, last);}Factory和Service看起來(lái)非常相似, 以至于很多開(kāi)發(fā)人員都無(wú)法理解它們有什么不同.
當實(shí)例化一個(gè) .service() 的時(shí)候, 其實(shí)他將通過(guò)調用 new Service() 的形式來(lái)給我們創(chuàng )建一個(gè)新的實(shí)例, .service() 的方法像是一個(gè)構造函數.
服務(wù)(service)實(shí)際上來(lái)說(shuō)是一個(gè)最基本的工廠(chǎng)(factory), 但是它是通過(guò) new 來(lái)創(chuàng )建的, 你需要使用 this 來(lái)添加你需要的變量和函數, 最后返回這個(gè)對象.
工廠(chǎng)(factory)實(shí)際上是非常接近面向對象中的"工廠(chǎng)模式(factory pattern)". 當你調用時(shí), 它會(huì )創(chuàng )建新的實(shí)例. 本質(zhì)上來(lái)說(shuō), 那個(gè)實(shí)例是一個(gè)全新的對象.
下面是Angular內部實(shí)際執行的源碼:
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 又是通過(guò) new Scope() 來(lái)創(chuàng )建的. 所有的子scope都是用過(guò)調用 $scope.$new() 來(lái)創(chuàng )建的.
var $rootScope = new Scope();它內部有一個(gè) $new 方法, 讓新的scope可以從原型鏈上引用它們的父scope, 子scope(為了digest cycle), 以及前后的scope.
從下面的代碼可以看出, 如果你想創(chuàng )建一個(gè)獨立的scope, 那么你應該使用 new Scope(), 否則它將以繼承的方式來(lái)創(chuàng )建.
我省略了一些不必要的代碼, 下面是他的核心實(shí)現
$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; }理解這一點(diǎn)對寫(xiě)測試非常重要, 如果你想測試你的Controller, 那么你應該使用 $scope.$new() 來(lái)創(chuàng )建$scope對象. 明白scope是如何創(chuàng )建的在測試驅動(dòng)開(kāi)發(fā)(TDD)中是十分重要的, 這將更加有助于你mock module.
digest cycle的實(shí)現其實(shí)就是我們經(jīng)??吹降?$digest 關(guān)鍵字, Angular強大的雙向綁定功能依賴(lài)于它. 每當一個(gè)model被更新時(shí)他都會(huì )運行, 檢查當前值, 如果和以前的不同, 將觸發(fā)listener. 這些都是臟檢查(dirty checking)的基礎內容. 他會(huì )檢查所有的model, 與它們原來(lái)的值進(jìn)行比較, 如果不同, 觸發(fā)listener, 循環(huán), 直到不在有變化為止.
$scope.name = 'Todd';$scope.$watch(function() { return $scope.name;}, function (newValue, oldValue) { console.log('$scope.name was updated!');} );當你調用 $scope.$watch 的時(shí)候, 實(shí)際上干了2件事情. watch的第一個(gè)參數是一個(gè)function, 這個(gè)function的返回你想監控的對象(如果你傳遞的是一個(gè)string, Angular會(huì )把他轉換為一個(gè)function). digest cycle 運行的時(shí)候, 它會(huì )調用這個(gè)function. 第二個(gè)參數也是一個(gè)function, 當第一個(gè)function的值發(fā)生變化的時(shí)候它會(huì )被調用. 讓我們看看他是怎么實(shí)現監控的:
$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; }; }這個(gè)方法將會(huì )把參數添加到scope中的 $$watchers 數組中, 并且它會(huì )返回一個(gè)function, 以便于你想結束這個(gè)監控操作.
然后digest cycle會(huì )在每次調用 $scope.$apply 或者 $scope.$digest 的時(shí)候運行. $scope.$apply 實(shí)際上是一個(gè)rootScope的包裝, 他會(huì )從根$rootScope向下廣播. 而 $scope.$digest 只會(huì )在當前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); } } }這個(gè)實(shí)現非常有才, 雖然我沒(méi)有進(jìn)去看它是如何向下級廣播的, 但這里的關(guān)鍵是循環(huán)遍歷 $$watchers, 執行里面的函數(就是那個(gè)你通過(guò) $scope.$watch 注冊的第一個(gè)function), 然后如果得到和之前不同的值, 他又將調用listener(那個(gè)你傳遞的第二個(gè)function). 然后, 砰! 我們得到了一個(gè)變量發(fā)生改變的通知. 關(guān)鍵是我們是如何知道一個(gè)值發(fā)生變化了的? 當一個(gè)值被更新的時(shí)候digest cycle會(huì )運行(盡管它可能不是必須的). 比如在 ng-model 上, 每一個(gè)keydown事件都會(huì )觸發(fā)digest cycle.
當你想在A(yíng)ngular框架之外做點(diǎn)什么的時(shí)候, 比如在 setTimeout 的方法里面你想讓Angular知道你可能改變了某個(gè)model的值. 那么你需要使用 $scope.$apply, 你把一個(gè)function放在它的參數之中, 那么他會(huì )在A(yíng)ngular的作用域運行它, 然后在 $rootScope 上調用 $digest. 它將向它下面所有的scope進(jìn)行廣播, 這將觸發(fā)你注冊的所有listeners和watchers. 這一點(diǎn)意味著(zhù)Angular可以知道你更新了任何作用域的變量.
Angular實(shí)現polyfilling的方式非常巧妙, 它不是用像 Function.prototype.bind 一樣的方式直接綁定在一個(gè)對象的原型鏈上. Angular會(huì )調用一個(gè)function來(lái)判定瀏覽器是否支持這個(gè)方法(基礎特征檢查), 如果存在它會(huì )直接返回這個(gè)方法. 如果不存在, 他將使用一段簡(jiǎn)短的代碼來(lái)實(shí)現它.
這樣是比較安全的方式. 如果直接在原型鏈上綁定方法, 那么它可能會(huì )覆蓋其它類(lèi)庫或者框架的代碼(甚至是我們自己的代碼). 閉包也讓我們可以更安全的儲存和計算那些臨時(shí)變量, 如果存在這個(gè)方法, Angular將直接調用. 原生方法通常會(huì )帶來(lái)極大的性能提升.
Angular支持IE8+的瀏覽器(撰寫(xiě)本文時(shí)Angular版本是1.2.x), 這意味著(zhù)它還是要兼容老的瀏覽器, 為它們提供那些沒(méi)有的功能. 讓我們來(lái)用 indexOf 來(lái)舉例.
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;}它直接取代了原來(lái)的 array.indexOf 方法, 它自己實(shí)現了indexOf方法. 但如果瀏覽器支持這個(gè)函數, 他將直接調用原生方法. 十分簡(jiǎn)單.
實(shí)現閉包可以用一個(gè)立即執行函數(IIFE). 比如下面這個(gè) isArray 方法, 如果瀏覽器不支持這個(gè)功能, 它將使用閉包返回一個(gè) Array.isArray 的實(shí)現. 如果 Array.isArray 是一個(gè)函數, 那么它將直接使用原生方法----又一個(gè)提高性能的方法. IIFE可以讓我們十分的方便來(lái)封裝一些東西, 然后只返回我們需要的內容.
var isArray = (function() { if (!isFunction(Array.isArray)) { return function(value) { return toString.call(value) === '[object Array]'; }; } return Array.isArray;})();這就是我看的第一部分Angular源碼, 第二部分將在下周發(fā)布.
聯(lián)系客服