注: 文中所有涉及到的AngularJS源码均来自angular-1.2.8版本。
注2: 本文结构为一段代码配一段说明,代码在上,说明在下。
从3月3号到现在,进入新公司也有1个月了。过去这一个月里主要负责公司内部某个DevOps工具开发过程中的前端部分。由于涉及到较多的网页交互,该项目的前端部分使用了AngularJS这个框架。本人这正好借助这个机会进一步了解了AngularJS的相关知识。
这两天项目主体完成,准备上线,算是有了一些自由时间,正好借此机会学习一下AngularJS内部的实现机制。
本篇来说说最常见的directive之一 ngRepeat
该文件的整体结构如下:
var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) {
var NG_REMOVED = '$$NG_REMOVED';
var ngRepeatMinErr = minErr('ngRepeat');
return {
transclude: 'element',
priority: 1000,
terminal: true,
$$tlb: true,
link: function($scope, $element, $attr, ctrl, $transclude){
.....
}
};
function getBlockStart(block) {
return block.clone[0];
}
function getBlockEnd(block) {
return block.clone[block.clone.length - 1];
}
}];
ngRepeatDirective
是一个数组,它会被传递给directive方法用来生成ngRepeat这个directive。可以看到ngRepeat依赖于$parse
和$animate
两个服务。$parse
用来将字符串解析成javascript函数。$animate
用来给DOM改变附加上动画效果。文件最后声明了两个helper方法,会在稍后的分析中介绍到。
接下来把最大的篇幅交给这个directive的核心——link方法。
var expression = $attr.ngRepeat;
var match = expression.match(/^\s*([\s\S]+?)\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?\s*$/),
trackByExp, trackByExpGetter, trackByIdExpFn, trackByIdArrayFn, trackByIdObjFn,
lhs, rhs, valueIdentifier, keyIdentifier,
hashFnLocals = {$id: hashKey};
if (!match) {
throw ngRepeatMinErr('iexp', "Expected expression in form of '_item_ in _collection_[ track by _id_]' but got '{0}'.",
expression);
}
lhs = match[1];
rhs = match[2];
trackByExp = match[3];
首先从directive元素的attribute中取出ng-repeat
属性设置的字符串。接着会尝试用取到的字符串与一个正则表达式匹配,用来检查ng-repeat语句是否符合语法。如果语法正确,则将字符串拆分为3部分。lhs
保存了循环中的临时变量的名字,rhs
保存了被循环的collection的名字,trackByExp
则保存了可选的track by
字符串。
if (trackByExp) {
trackByExpGetter = $parse(trackByExp);
trackByIdExpFn = function(key, value, index) {
// assign key, value, and $index to the locals so that they can be used in hash functions
if (keyIdentifier) hashFnLocals[keyIdentifier] = key;
hashFnLocals[valueIdentifier] = value;
hashFnLocals.$index = index;
return trackByExpGetter($scope, hashFnLocals);
};
} else {
trackByIdArrayFn = function(key, value) {
return hashKey(value);
};
trackByIdObjFn = function(key) {
return key;
};
}
如果指定了trackByExp
则根据该字符串构造一个id生成函数,否则为数组和哈希构造缺省的id生成函数。这些函数生成的id会被用来把每个item与页面中唯一的元素绑定。
match = lhs.match(/^(?:([\$\w]+)|\(([\$\w]+)\s*,\s*([\$\w]+)\))$/);
if (!match) {
throw ngRepeatMinErr('iidexp', "'_item_' in '_item_ in _collection_' should be an identifier or '(_key_, _value_)' expression, but got '{0}'.",
lhs);
}
valueIdentifier = match[3] || match[1];
keyIdentifier = match[2];
// Store a list of elements from previous run. This is a hash where key is the item from the
// iterator, and the value is objects with following properties.
// - scope: bound scope
// - element: previous element.
// - index: position
var lastBlockMap = {};
判断临时变量的格式,angular支持两种格式album in artist.albums
用来循环一个数组,(key, value) in expression
用来循环一个哈希。两种格式中都有表示将要作为临时变量值的valueIdentifier
,而只有在第二种情况中有作为临时变量键的keyIdentifier
。
lastBlockMap
是一个用来保存最近一次view更新完后各个循环item状态的hash。该hash的键是各个被循环的item,值则是一个保存了该item相应属性的对象:
- scope 与该item绑定的scope
- element view中在该元素之前的元素
- index item对应的元素在页面中的出现顺序,也即最后一次循环时,该item被处理的先后顺序。
$scope.$watchCollection(rhs, function ngRepeatAction(collection){
var index, length,
previousNode = $element[0], // current position of the node 初始值为angular创建的一个helper元素,在页面中表现为一段注释,例如<!-- ngRepeat: item in items -->
nextNode,
// Same as lastBlockMap but it has the current state. It will become the
// lastBlockMap on the next iteration.
nextBlockMap = {},
arrayLength,
childScope,
key, value, // key/value of iteration
trackById,
trackByIdFn,
collectionKeys,
block, // last object information {scope, element, id}
nextBlockOrder = [],
elementsToRemove;
接着便是link方法最核心的部分——监听model变化并更新view。这里使用了$watchCollection
方法监听我们指定的collection,并在每次collection更新时调用ngRepeatAction方法。
if (isArrayLike(collection)) {
collectionKeys = collection;
trackByIdFn = trackByIdExpFn || trackByIdArrayFn;
} else {
trackByIdFn = trackByIdExpFn || trackByIdObjFn;
// if object, extract keys, sort them and use to determine order of iteration over obj props
collectionKeys = [];
for (key in collection) {
if (collection.hasOwnProperty(key) && key.charAt(0) != '$') {
collectionKeys.push(key);
}
}
collectionKeys.sort();
}
arrayLength = collectionKeys.length;
首先确定collectionKeys
和trackByIdFn
。为将要被循环的集合中的每一个item创建一个key,保存在collectionKeys
中。如果collection是数组,则key就是item自身;如果collection是对象,则每个键值对的键会被用作对于item的key(同时会对key排序,排序的结果就是collection中item在页面上显示的顺序)
arrayLength用来保存本次将要循环的item的数目。也就是此次更新后页面中显示的item的数目。
// locate existing items
length = nextBlockOrder.length = collectionKeys.length;
for(index = 0; index < length; index++) {
key = (collection === collectionKeys) ? index : collectionKeys[index];
value = collection[key];
trackById = trackByIdFn(key, value, index);
assertNotHasOwnProperty(trackById, '`track by` id');
if(lastBlockMap.hasOwnProperty(trackById)) {
block = lastBlockMap[trackById];
delete lastBlockMap[trackById];
nextBlockMap[trackById] = block;
nextBlockOrder[index] = block;
} else if (nextBlockMap.hasOwnProperty(trackById)) {
// restore lastBlockMap
forEach(nextBlockOrder, function(block) {
if (block && block.scope) lastBlockMap[block.id] = block;
});
// This is a duplicate and we need to throw an error throw ngRepeatMinErr('dupes', "Duplicates in a repeater are not allowed. Use 'track by' expression to specify unique keys. Repeater: {0}, Duplicate key: {1}",
nextBlockOrder[index] = { id: trackById };
nextBlockMap[trackById] = false;
}
}
按照collectionKeys
中保存的key依次取出要被循环处理的value。trackById
是使用trackByIdFn
计算出来的每个item唯一的标识,用来建立item与页面中元素间的关联。
如果lastBlockMap
中有trackById
这个属性,则说明该item在上次循环中已经存在,则将相应的属性/值设置到nextBlockMap
对象中,同时在nextBlockOrder
数组中保存顺序。
如果在lastBlockMap
中找不到trackById
但在nextBlockMap
中找到了,则说明在collection中有两个item的trackById
是相同的,这时会抛出异常,因为不可能两个item对应页面中的同一个element。
如果在两个map对象中都没有找到,则说明这个item是首次出现,那么则在nextBlockMap
中将对应的值设置为false,表明没有scope与之对应。
// remove existing items
for (key in lastBlockMap) {
// lastBlockMap is our own object so we don't need to use special hasOwnPropertyFn
if (lastBlockMap.hasOwnProperty(key)) {
block = lastBlockMap[key];
elementsToRemove = getBlockElements(block.clone);
$animate.leave(elementsToRemove);
forEach(elementsToRemove, function(element) { element[NG_REMOVED] = true; }); // 标记该元素已经从DOM中移除
block.scope.$destroy(); // 销毁对应的scope
}
}
在经历前面一次检查后,现在还留在lastBlockMap
中的item就是被从collection中移除的item。要做的就是将对应的element从DOM中移除并销毁item对应的scope。
// we are not using forEach for perf reasons (trying to avoid #call)
for (index = 0, length = collectionKeys.length; index < length; index++) {
key = (collection === collectionKeys) ? index : collectionKeys[index];
value = collection[key];
block = nextBlockOrder[index];
if (nextBlockOrder[index - 1]) previousNode = getBlockEnd(nextBlockOrder[index - 1]);
if (block.scope) {
// if we have already seen this object, then we need to reuse the
// associated scope/element
childScope = block.scope; // 如果是已经存在的item,则使用之前生成的scope
nextNode = previousNode;
do {
nextNode = nextNode.nextSibling;
} while(nextNode && nextNode[NG_REMOVED]); // 跳过被标记为已删除的元素
if (getBlockStart(block) != nextNode) { // 如果当前block对应的元素并没有紧接在previousNode后边
// existing item which got moved
$animate.move(getBlockElements(block.clone), null, jqLite(previousNode)); // 那么就将元素移动到previousNode之后的位置
}
previousNode = getBlockEnd(block); // 同时设置previousNode为当前循环中的元素,为下一次循环做准备
} else {
// new item which we don't know about
childScope = $scope.$new(); // 如果是新item,生成一个新scope
}
最后一个循环用来处理已有item的DOM移动以及新item对应的DOM插入。在这个循环中previousNode
代表了上一次循环item元素在DOM中的位置,angular会顺次将各个block插入到前一个block的后面(对于已经存在的元素则是移动)。
childScope[valueIdentifier] = value;
if (keyIdentifier) childScope[keyIdentifier] = key;
childScope.$index = index;
childScope.$first = (index === 0);
childScope.$last = (index === (arrayLength - 1));
childScope.$middle = !(childScope.$first || childScope.$last);
// jshint bitwise: false
childScope.$odd = !(childScope.$even = (index&1) === 0);
// jshint bitwise: true
接着设置了一些方便在directive中方便判断状态的变量。如当前元素是否是第一个/最后一个出现在页面上的item。方便用户为相应item元素做特殊处理。
if (!block.scope) {
$transclude(childScope, function(clone) {
clone[clone.length++] = document.createComment(' end ngRepeat: ' + expression + ' ');
$animate.enter(clone, null, jqLite(previousNode));
previousNode = clone;
block.scope = childScope;
// Note: We only need the first/last node of the cloned nodes.
// However, we need to keep the reference to the jqlite wrapper as it might be changed later
// by a directive with templateUrl when it's template arrives.
block.clone = clone;
nextBlockMap[block.id] = block;
});
}
}
lastBlockMap = nextBlockMap;
对于新item,将新生成的scope应用到template上生成相应的元素,并插入DOM中。同时设置previousNode
为新生成的元素,并将scope等相关信息保存至nextBlockMap
数组中。
最后将nextBlockMap的值赋给lastBlockMap,结束本次循环。
以上就是对ngRepeat代码的简要分析,顺手画了一个简单说明ngRepeat工作方式的流程图。可以与上文的分析结合来看:
如果有任何问题,欢迎留言讨论。