当你用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源码, 第二部分将在下周发布.