當你用AngularJS寫的應用越多, 你會越發的覺得它相當神奇. 之前我用AngularJS實現了相當多酷炫的效果, 所以我決定去看看它的源碼, 我想這樣也許我能知道它的原理. 下面是我從源碼中找到的一些可以了解AngularJS那些高級(和隱藏)功能如何實現的代碼.
1) 依賴注入的實現原理
依賴注入(DI)讓我們可以不用自己實例化就能創建依賴對象的方法. 簡單的來說, 依賴是以注入的方式傳遞的. 在Web應用中, Angular讓我們可以通過DI來創建像Controllers和Directives這樣的對象. 我們還可以創建自己的依賴對象, 當我們要實例化它們時, Angular能自動實現注入.
最常見的被注入對象應該是 $scope
對象. 它可以像下面這樣被注入的:
function MainCtrl ($scope) {
// access to $scope
}
angular
.module(‘app’)
.controller(‘MainCtrl’, MainCtrl);
對於從來沒有接觸過依賴注入的Javascript開發人員來說, 這樣看起來只是像傳遞了一個參數. 而實際上, 他是一個依賴注入的占位符. Angular通過這些占位符, 把真正的對象實例化給我們, 讓來看看他是怎么實現的.
function的參數
當你運行你代碼的時候, 如果你把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很強大的地方. 轉換后我們將得到如下字符串:
‘function MainCtrl ($scope) {...}’
然后, 他用正則移除了在 function()
中有可能的所有的注釋.
fnText = fn.toString().replace(STRIP_COMMENTS, '');
接着它提取其中的參數部分.
argDecl = fnText.match(FN_ARGS);
最后它使用 .split()
方法來移除參數中的所有空格, 完美! Angular使用一個內部的 forEach
方法來遍歷這些參數, 然后把他們放入一個 $inject
數組中.
forEach(argDecl[1].split(FN_ARG_SPLIT), function(arg) {
arg.replace(FN_ARG, function(all, underscore, name) {
$inject.push(name);
});
});
正如你現在想的, 這是一個很大的性能開銷操作. 每個函數都要執行4個正則表達式還有大量的轉換操作----這將給我們帶來性能損失. 不過我們可以通過直接添加需要注入的對象到 $inject
數組中的方式來避免這個開銷.
$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);
}
2) Factory和Service
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);
}]);
}
3) 從 $rootScope 中創建新的 $scope
所有的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.
4) Digest Cycle
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.
$scope.$apply
當你想在Angular框架之外做點什么的時候, 比如在 setTimeout
的方法里面你想讓Angular知道你可能改變了某個model的值. 那么你需要使用 $scope.$apply
, 你把一個function放在它的參數之中, 那么他會在Angular的作用域運行它, 然后在 $rootScope
上調用 $digest
. 它將向它下面所有的scope進行廣播, 這將觸發你注冊的所有listeners和watchers. 這一點意味着Angular可以知道你更新了任何作用域的變量.
通過特征檢查和閉包實現Polyfilling
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源碼, 第二部分將在下周發布.