Angularjs 源码阅读体会

说明

  • 本文是我在阅读Angularjs源码时的一些体会,写出来分享给大家,不足之处请多多指正。
  • 本文以angularjs 1.5.x版本源码为例;
  • 本文参考:文章一系列分析文章

Angularjs在初始化的时候做了什么

尝试绑定jQuery对象,如果没有则采用内置的jqLite(30561行):

1
2
3
//try to bind to jquery now so that one can write jqLite(document).ready()
//but we will rebind on bootstrap again.
bindJQuery();

publishExternalAPI初始化angular环境,这也是angularjs实现其重要特性–扩展指令、依赖注入的重要步骤:

  • 把一些基础api挂载到angular上(2455行):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
extend(angular, {
'bootstrap': bootstrap,
'copy': copy,
'extend': extend,
'merge': merge,
'equals': equals,
'element': jqLite,
'forEach': forEach,
'injector': createInjector,
'noop': noop,
'bind': bind,
'toJson': toJson,
'fromJson': fromJson,
'identity': identity,
'isUndefined': isUndefined,
'isDefined': isDefined,
'isString': isString,
'isFunction': isFunction,
'isObject': isObject,
'isNumber': isNumber,
'isElement': isElement,
'isArray': isArray,
'version': version,
'isDate': isDate,
'lowercase': lowercase,
'uppercase': uppercase,
'callbacks': {counter: 0},
'getTestability': getTestability,
'$$minErr': minErr,
'$$csp': csp,
'reloadWithDebugInfo': reloadWithDebugInfo
});
  • 在angular上添加了module方法(2488行):
    • 在window下面创建全局的angular对象,并且返回一个高阶函数,赋值给了angular.module属性;
    • setupModuleLoader返回的内容中包含了providerfactoryservicevalueconstantcontrollerdirective等诸多对象,方便用angular.module链式调用这些对象;
1
angularModule = setupModuleLoader(window);
1
2
3
4
5
6
7
//链式调用
angular.module('demoApp', [])
.factory()
.controller()
.directive()
.config()
.run();

当dom ready时,开始执行程序的初始化

  • 执行初始化的代码(30708行):
1
2
3
jqLite(document).ready(function() {
angularInit(document, bootstrap);
});
  • angularInit函数(1591行):
    • 获取ng-app的应用根节点(含有xx-app属性的dom);
    • 获取启动模块(xx-app的值xx为 [‘ng-‘, ‘data-ng-‘, ‘ng:’, ‘x-ng-‘] 任一一种;
    • 将上述结果传给bootstrap函数:bootstrap(appElement, module ? [module] : [], config);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// The element `element` has priority over any other element
forEach(ngAttrPrefixes, function(prefix) {
var name = prefix + 'app';

if (!appElement && element.hasAttribute && element.hasAttribute(name)) {
appElement = element;
module = element.getAttribute(name);
}
});
forEach(ngAttrPrefixes, function(prefix) {
var name = prefix + 'app';
var candidate;

if (!appElement && (candidate = element.querySelector('[' + name.replace(':', '\\:') + ']'))) {
appElement = candidate;
module = candidate.getAttribute(name);
}
});
  • boostrap函数(1679行):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
function bootstrap(element, modules, config) {
// ...省略若干代码
var doBootstrap = function() {
element = jqLite(element);

// 首先判断该dom是否已经被注入(即这个dom已经被bootstrap过)
// 注意这里的injector方法是Angular为jqLite中提供的,区别于一般的jQuery api
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);
}]);
}

// 添加ng模块
modules.unshift('ng');

// 到这里modules可能是: ['ng', [$provide, function($provide){...}], 'xx']
// xx: ng-app="xx"

// 创建injector对象,注册所有的内置模块
var injector = createInjector(modules, config.strictDi);

// 利用injector的依赖注入,执行回调
injector.invoke(['$rootScope', '$rootElement', '$compile', '$injector',
function bootstrapApply(scope, element, compile, injector) {
scope.$apply(function() {
// 标记该dom已经被注入
element.data('$injector', injector);
// 编译整个dom
compile(element)(scope);
});
}]
);
return injector;
};

// ...省略若干代码

if (window && !NG_DEFER_BOOTSTRAP.test(window.name)) {
return doBootstrap();
}
// ...省略若干代码
}
  • 除了系统会自动调用bootsrap函数,也可以自己手动调用:
1
2
3
4
5
angular.module('demo', [])
.controller('WelcomeController', function($scope) {
$scope.greeting = 'Welcome!';
});
angular.bootstrap(document, ['demo']);

$get的分析

  • $get见源码16163行;
  • 构造函数如下:
1
2
3
4
5
6
7
8
9
10
11
12
function Scope() {
this.$id = nextUid();
this.$$phase = this.$parent = this.$$watchers =
this.$$nextSibling = this.$$prevSibling =
this.$$childHead = this.$$childTail = null;
this.$root = this;
this.$$destroyed = false;
this.$$listeners = {};
this.$$listenerCount = {};
this.$$watchersCount = 0;
this.$$isolateBindings = null;
}
  • 含义:

    • $id, 通过nextUid方法来生成一个唯一的标识
    • $$phase, 这是一个状态标识,一般在dirty check时用到,表明现在在哪个阶段
    • $parent, 代表自己的上级scope属性
    • $$watchers, 保存scope变量当前所有的监控数据,是一个数组
    • $$nextSibling, 下一个兄弟scope属性
    • $$prevSibling, 前一个兄弟scope属性
    • $$childHead, 第一个子级scope属性
    • $$childTail, 最后一个子级scope属性
    • $$destroyed, 表示是否被销毁
    • $$asyncQueue, 代表异步操作的数组
    • $$postDigestQueue, 代表一个在dirty check之后执行的数组
    • $$listeners, 代表scope变量当前所有的监听数据,是一个数组
  • 创建子级作用域是通过$new方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
$new: function(isolate, parent) {
var child;

parent = parent || this;

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 = createChildScopeClass(this);
}
child = new this.$$ChildScope();
}
child.$parent = parent;
child.$$prevSibling = parent.$$childTail;
if (parent.$$childHead) {
parent.$$childTail.$$nextSibling = child;
parent.$$childTail = child;
} else {
parent.$$childHead = parent.$$childTail = child;
}

// When the new scope is not isolated or we inherit from `this`, and
// the parent scope is destroyed, the property `$$destroyed` is inherited
// prototypically. In all other cases, this property needs to be set
// when the parent scope is destroyed.
// The listener needs to be added after the parent is set
if (isolate || parent != this) child.$on('$destroy', destroyChildScope);

return child;
}
  • 分析:
    • isolate标识来创建独立作用域,这个在创建指令,并且scope属性定义的情况下,会触发这种情况,还有几种别的特殊情况,假如是独立作用域的话,会多一个$root属性,这个默认是指向rootscope的;
    • 如果不是独立的作用域,则会生成一个内部的构造函数,把此构造函数的prototype指向当前scope实例;
    • 通用的操作就是,设置当前作用域的$$childTail,$$childTail.$$nextSibling,$$childHead,this.$$childTail为生成的子级作用域;设置子级域的$parent为当前作用域,$$prevSibling为当前作用域最后一个子级作用域;

$watch的分析

  • $watch在16452行,具体如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
$watch: function(watchExp, listener, objectEquality, prettyPrintExpression) {
var get = $parse(watchExp);

if (get.$$watchDelegate) {
return get.$$watchDelegate(this, listener, objectEquality, get, watchExp);
}
var scope = this,
array = scope.$$watchers,
watcher = {
fn: listener,
last: initWatchVal,
get: get,
exp: prettyPrintExpression || 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);
incrementWatchersCount(this, 1);

return function deregisterWatch() {
if (arrayRemove(array, watcher) >= 0) {
incrementWatchersCount(scope, -1);
}
lastDirtyWatch = null;
};
}
  • 对象含义:

    • fn, 代表监听函数,当监控表达式新旧不相等时会执行此函数
    • last, 保存最后一次发生变化的监控表达式的值
    • get, 保存一个监控表达式对应的函数,目的是用来获取表达式的值然后用来进行新旧对比的
    • exp, 保存一个原始的监控表达式
    • eq, 保存$watch函数的第三个参数,表示是否进行深度比较
  • 检查传递进来的监听参数是否为函数,如果是一个有效的字符串,则通过parse来解析生成一个函数,否则赋值为一个noop占位函数,最后生成一个包装函数,函数体的内容就是执行刚才生成的监听函数,默认传递当前作用域.

  • 检查监控表达式是否为字符串并且执行表达式的constant为true,代表这个字符串是一个常量,那么,系统在处理这种监听的时候,执行完一次监听函数之后就会删除这个$watch.最后往当前作用域里的$$watchers数组头中添加$watch信息,注意这里的返回值,利用JS的闭包保留了当前的watcher,然后返回一个函数,这个就是用来删除监听用的.

$digest分析

  • 见源码16812行;
  • digest方法是dirty check的核心,主要思路是先执行$$asyncQueue队列中的表达式,然后开启一个loop来的执行所有的watch里的监听函数,前提是前后两次的值是否不相等,假如ttl超过系统默认值,则dirth check结束,最后执行$$postDigestQueue队列里的表达式.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
$digest: function() {
var watch, value, last, fn, get,
watchers,
length,
dirty, ttl = TTL,
next, current, target = this,
watchLog = [],
logIdx, asyncTask;

beginPhase('$digest');
// Check for changes to browser url that happened in sync before the call to $digest
$browser.$$checkUrlChange();

if (this === $rootScope && applyAsyncId !== null) {
// If this is the root scope, and $applyAsync has scheduled a deferred $apply(), then
// cancel the scheduled $apply and flush the queue of expressions to be evaluated.
$browser.defer.cancel(applyAsyncId);
flushApplyAsync();
}

lastDirtyWatch = null;

do { // "while dirty" loop
dirty = false;
current = target;

while (asyncQueue.length) {
try {
asyncTask = asyncQueue.shift();
asyncTask.scope.$eval(asyncTask.expression, asyncTask.locals);
} catch (e) {
$exceptionHandler(e);
}
lastDirtyWatch = null;
}

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) {
get = watch.get;
if ((value = 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;
fn = watch.fn;
fn(value, ((last === initWatchVal) ? value : last), current);
if (ttl < 5) {
logIdx = 4 - ttl;
if (!watchLog[logIdx]) watchLog[logIdx] = [];
watchLog[logIdx].push({
msg: isFunction(watch.exp) ? 'fn: ' + (watch.exp.name || watch.exp.toString()) : watch.exp,
newVal: value,
oldVal: last
});
}
} 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);
}
}
}

// Insanity Warning: scope depth-first traversal
// yes, this code is a bit crazy, but it works and we have tests to prove it!
// this piece should be kept in sync with the traversal in $broadcast
if (!(next = ((current.$$watchersCount && current.$$childHead) ||
(current !== target && current.$$nextSibling)))) {
while (current !== target && !(next = current.$$nextSibling)) {
current = current.$parent;
}
}
} 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, watchLog);
}

} while (dirty || asyncQueue.length);

clearPhase();

while (postDigestQueue.length) {
try {
postDigestQueue.shift()();
} catch (e) {
$exceptionHandler(e);
}
}
}
  • 核心就是两个loop,外loop保证所有的model都能检测到,内loop则是真实的检测每个watch,watch.get就是计算监控表达式的值,这个用来跟旧值进行对比,假如不相等,则执行监听函数;
  • 比较完之后,把新值传给watch.last,然后执行watch.fn也就是监听函数,传递三个参数,分别是:最新计算的值,上次计算的值(假如是第一次的话,则传递新值),最后一个参数是当前作用域实例,这里有一个设置外loop的条件值,那就是dirty = true,也就是说只要内loop执行了一次watch,则外loop还要接着执行,这是为了保证所有的model都能监测一次,虽然这个有点浪费性能,不过超过ttl设置的值后,dirty check会强制关闭,并抛出异常;
  • 当检查完一个作用域内的所有watch之后,则开始深度遍历当前作用域的子级或者父级,虽然这有些影响性能,就像这里的注释写的那样yes, this code is a bit crazy

$apply分析

  • 见源码17121行;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$apply: function(expr) {
try {
beginPhase('$apply');
try {
return this.$eval(expr);
} finally {
clearPhase();
}
} catch (e) {
$exceptionHandler(e);
} finally {
try {
$rootScope.$digest();
} catch (e) {
$exceptionHandler(e);
throw e;
}
}
}
  • 这个方法一般用在,不在ng的上下文中执行js代码的情况,比如原生的DOM事件中执行想改变ng中某些model的值,这个时候就要使用$apply方法了;
  • 代码中,首先让当前阶段标识为$apply,这个可以防止使用$apply方法时检查是否已经在这个阶段了,然后就是执行$eval方法, 这个方法上面有讲到,最后执行$digest方法,来使ng中的M或者VM改变.
您的支持是对我最大的鼓励!