分享

启动和JQuery绑定--AngularJS学习笔记(二)

 看见就非常 2014-10-31

By

更新日期:

上一篇简单的分析了AngularJS的项目结构,后面就开始分析具体的源代码了。

从angularFiles.js中的定义可以看出有几个文件直接位于src根目录,并不是隶属于某个模块。这几 个分别是minErr.js,Angular.js,loader.js,AngularPublic.js,jqLite.js,apis.js。

这几个提供了AngularJS中很基础的工具,比如angular.equals等,在文档中它们中的部分被归属于 Global API中。

本文主要看看AngularJS的启动和JQuery绑定。

minErr.js

Javascript中有Error对象,用于表示运行时错误,但是它的功能比较单一,一般使用

1
new Error(message)

但是它的功能比较单一。而minErr是一个提供更丰富信息的工具。这样生成的错误信息将包含模块名 称和其他信息,同时还提供了模板功能。比如

1
2
3
var exampleMinErr = minErr('example');
var detailMinErr=exampleMinErr('one', 'This {0} is {1}', "v1", "v2");
detailMinErr.stack

显示如下:

screenshot13screenshot13

可以看出错误还提供了对应了文档地址,不过其中的NG_VERSION_FULL看着有点奇怪,这里应该显示诸如1.2.7这样的版本号。在 lib/grunt/utils.js中有一个处理,将NG_VERSION_FULL、NG_VERSION_MAJOR等替换了。

1
2
3
4
5
6
7
8
9
10
11
function(src, NG_VERSION, strict){
var processed = src
.replace(/"NG_VERSION_FULL"/g, NG_VERSION.full)
.replace(/"NG_VERSION_MAJOR"/, NG_VERSION.major)
.replace(/"NG_VERSION_MINOR"/, NG_VERSION.minor)
.replace(/"NG_VERSION_DOT"/, NG_VERSION.dot)
.replace(/"NG_VERSION_CDN"/, NG_VERSION.cdn)
.replace(/"NG_VERSION_CODENAME"/, NG_VERSION.codename);
if (strict !== false) processed = this.singleStrict(processed, '\n\n', true);
return processed;
}

这里的NG_VERSION信息是getVersion从package.json文件的version值中读取的。

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
function(){
if (version) return version;
var package = JSON.parse(fs.readFileSync('package.json', 'UTF-8'));
var match = package.version.match(/^([^\-]*)(?:\-(.+))?$/);
var semver = match[1].split('.');
var fullVersion = match[1];
if (match[2]) {
fullVersion += '-';
fullVersion += (match[2] == 'snapshot') ? getSnapshotSuffix() : match[2];
}
version = {
full: fullVersion,
major: semver[0],
minor: semver[1],
dot: semver[2].replace(/rc\d+/, ''),
codename: package.codename,
cdn: package.cdnVersion
};
return version;
}

现在来看看minErr的源码,其实主要是模板替换功能。

1
2
3
message = prefix + template.replace(/\{\d+\}/g, function (match) {
...
}

这里的prefix是module名称组成而成的,而这里使用了replace方法,匹配诸如{0},{1}的字符串,而 /g命令表示全局替换。而 function(match)是根据匹配的字符串生成对应的替换字符串。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function (match) {
var index = +match.slice(1, -1), arg;
if (index + 2 < templateArgs.length) {
arg = templateArgs[index + 2];
if (typeof arg === 'function') {
return arg.toString().replace(/ ?\{[\s\S]*$/, '');
} else if (typeof arg === 'undefined') {
return 'undefined';
} else if (typeof arg !== 'string') {
return toJson(arg);
}
return arg;
}
return match;
}

因为传入的参数实质上是类似{0},{1}这样的字符串,所以首先用slice(1,-1)去掉两端的大括号,然 后比较剩余的数字范围是否正确,这里的+2是因为传入的参数中第一个是code码,第二个是模板,从第 三个开始才是替换用的字符串。这里还针对不同的类型有不同的处理。

Angular.js

这个文件涉及到了AngularJS的初始化,还提供了若干基础方法,比如大小写转化,浏览器适配, forEach,Uid生成,浅复制,深复制等。

其实这些基础方法比较简单,这里只分析nextUid方法。调用它可以生成一个供Angular使用的唯一ID 。初始的uid为

1
uid = ['0', '0', '0'];

Angular的uid不是单纯的数字,而是字母和数字的组合,这样就不会增长太快,也不会超值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function nextUid() {
var index = uid.length;
var digit;
while(index) {
index--;
digit = uid[index].charCodeAt(0);
if (digit == 57 /*'9'*/) {
uid[index] = 'A';
return uid.join('');
}
if (digit == 90 /*'Z'*/) {
uid[index] = '0';
} else {
uid[index] = String.fromCharCode(digit + 1);
return uid.join('');
}
}
uid.unshift('0');
return uid.join('');
}

首先是获取uid的长度,然后获取最后一位,如果是9就变成“A”,如果是Z就变成0,不 然就是直接+1。如果长度不够了就增加uid数组长度。这样首先生成的就是001,然后是002…然后 是00Z,接下来就是010。nextUid方法在Scope中会被使用。

然后AngularJS的启动了,这里就涉及了两个比较重要的方法:angularInit和bootstrap。

AngularJS官方文档的第一个列子就是一个数据绑定的例子,显示Hello {yourName}。

例子中html标签中添加了执行ng-app,文档说这是告知AngularJS,这里是它的管理范围,而实现就在angularInit中。

ng-app是一个自动初始化的指令,而且只能有一个自动初始化,其他的需要调用angular.bootstrap 启动。

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
function angularInit(element, bootstrap) {
var elements = [element],
appElement,
module,
names = ['ng:app', 'ng-app', 'x-ng-app', 'data-ng-app'],
NG_APP_CLASS_REGEXP = /\sng[:\-]app(:\s*([\w\d_]+);?)?\s/;
function append(element) {
element && elements.push(element);
}
forEach(names, function(name) {
names[name] = true;
append(document.getElementById(name));
name = name.replace(':', '\\:');
if (element.querySelectorAll) {
forEach(element.querySelectorAll('.' + name), append);
forEach(element.querySelectorAll('.' + name + '\\:'), append);
forEach(element.querySelectorAll('[' + name + ']'), append);
}
});
forEach(elements, function(element) {
if (!appElement) {
var className = ' ' + element.className + ' ';
var match = NG_APP_CLASS_REGEXP.exec(className);
if (match) {
appElement = element;
module = (match[2] || '').replace(/\s+/g, ',');
} else {
forEach(element.attributes, function(attr) {
if (!appElement && names[attr.name]) {
appElement = element;
module = attr.value;
}
});
}
}
});
if (appElement) {
bootstrap(appElement, module ? [module] : []);
}
}

该函数首先匹配指令本身,有4种ng:app,ng-app,x-ng-app,data-ng-app。我最开始挺好奇的为什么 有四种写法,只有一种不是挺好的嘛。网上搜寻了一番发现了原因。

首先是data-ng-app。在html5标准中定义了data-为自定义属性,这样页面就可以通过验证。x-也 是基于相似的原理。不过ng:app是为啥我确实没有找到…望知道的朋友点拨一下。ng:app是为了兼容XML,格式为namespace:name,其他三种依次为:none,HTML5,xHtml。

然后依次匹配这四种执行,直到appElement被赋值位置。确定了html元素位置以后通过正则表达式解 析module的名称,因为这是可选参数,所以如果没有提供就自动使用[]。当然具体的初始化实际上是有 bootstrap函数完成的。

首先是doBootstrap函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var doBootstrap = function() {
element = jqLite(element);
if (element.injector()) {
var tag = (element[0] === document) ? 'document' : startingTag(element);
throw ngMinErr('btstrpd', "App Already Bootstrapped with this Element '{0}'", tag);
}
modules = modules || [];
modules.unshift(['$provide', function($provide) {
$provide.value('$rootElement', element);
}]);
modules.unshift('ng');
var injector = createInjector(modules);
injector.invoke(['$rootScope', '$rootElement', '$compile', '$injector', '$animate',
function(scope, element, compile, injector, animate) {
scope.$apply(function() {
element.data('$injector', injector);
compile(element)(scope);
});
}]
);
return injector;
};

首先先将元素转为Jquery对象(如果没有,则有jqLite提供Jquery的一个功能子集),然后判断是否 有注入器了,是否已经初始化了。然后判断是否有modules,如果没有就为空。然后根据modules生成注 入器,然后编译。

jqLite.js

刚才启动的时候提到了将元素转为JQuery对象,但是很明显JQuery很流行,但不代表必须使用它。 AngularJS提供了jqLite,它提供了一个JQuery的子集,包含了addClass(),after(),append()等。当然 针对AngularJS的特定,它还提供了controller(),injector()等方法。

首先有个小适配,针对不同的浏览器对应事件的处理不同做了一个兼容:

1
2
3
4
5
6
addEventListenerFn = (window.document.addEventListener
function(element, type, fn) {element.addEventListener(type, fn, false);}
: function(element, type, fn) {element.attachEvent('on' + type, fn);}),
removeEventListenerFn = (window.document.removeEventListener
function(element, type, fn) {element.removeEventListener(type, fn, false); }
: function(element, type, fn) {element.detachEvent('on' + type, fn); });

其实主要是IE的问题,如果没有提供addEventListener方法,那么就使用attachEvent方法。

如果调用jqLite(element)方法,但是传入的是一个String的话那么就会自动生成一个html元素并插 入其中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
if (isString(element)) {
var div = document.createElement('div');
// Read about the NoScope elements here:
// http://msdn.microsoft.com/en-us/library/ms533897(VS.85).aspx
div.innerHTML = '<div> </div>' + element; // IE insanity to make
NoScope elements work!
div.removeChild(div.firstChild); // remove the superfluous div
jqLiteAddNodes(this, div.childNodes);
var fragment = jqLite(document.createDocumentFragment());
fragment.append(this); // detach the elements from the temporary DOM div.
} else {
jqLiteAddNodes(this, element);
}

如果没有使用JQuery,那么当然好,所有功能由jqLite提供。如果使用JQuery,那么这个适配其实是 由Angular.js处理的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function bindJQuery() {
// bind to jQuery if present;
jQuery = window.jQuery;
// reset to jQuery or default to us.
if (jQuery) {
jqLite = jQuery;
extend(jQuery.fn, {
scope: JQLitePrototype.scope,
isolateScope: JQLitePrototype.isolateScope,
controller: JQLitePrototype.controller,
injector: JQLitePrototype.injector,
inheritedData: JQLitePrototype.inheritedData
});
// Method signature:
// jqLitePatchJQueryRemove(name, dispatchThis, filterElems, getterIfNoArguments)
jqLitePatchJQueryRemove('remove', true, true, false);
jqLitePatchJQueryRemove('empty', false, false, false);
jqLitePatchJQueryRemove('html', false, false, true);
} else {
jqLite = JQLite;
}
angular.element = jqLite;
}

如果直接引用了JQuery,那么就会有一个window.jQuery。同时根据JQuery扩展的原理,将 controller(),injector()等方法加入JQuery中,然后调用jqLitePatchJQueryRemove将JQuery的remove 等和$destroy等绑定。

结语

还有loader.js,AngularPublic.js,apis.js没有分析,估摸着是下一篇的内容。

附上几个参考:

minError.js源码

JavaScript replace() 方法

关于data-*的规定

关于IE的attachEvent和IE11支持addEventListener

版权声明

《启动和JQuery绑定--AngularJS学习笔记(二)》黄云坤 创作,采用 知识共享 署名-相同方式共享 4.0 国际 许可协议进行许可。

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多