Angular.js源码分析之ngrepeat
在angular中一旦涉及到列表类的往往就轮到ng-repeat
这个指令出马了,其实就是循环数组或者对象,然后渲染得到内容。核心就是利用$transclude
,他可以被调用多次,而且会自动创建单独的scope,用$transclude
来做再好不过了。
首先来看下其源码:
var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) {
var NG_REMOVED = '$$NG_REMOVED';
var ngRepeatMinErr = minErr('ngRepeat');
var updateScope = function(scope, index, valueIdentifier, value, keyIdentifier, key, arrayLength) {
// 增加额外的一些属性值
scope[valueIdentifier] = value;
if (keyIdentifier) scope[keyIdentifier] = key;
scope.$index = index;
scope.$first = (index === 0);
scope.$last = (index === (arrayLength - 1));
scope.$middle = !(scope.$first || scope.$last);
// jshint bitwise: false
scope.$odd = !(scope.$even = (index&1) === 0);
// jshint bitwise: true
};
var getBlockStart = function(block) {
return block.clone[0];
};
var getBlockEnd = function(block) {
return block.clone[block.clone.length - 1];
};
return {
restrict: 'A',
multiElement: true, // 跨域多个元素可以
transclude: 'element',
priority: 1000,
terminal: true,
$$tlb: true,
compile: function ngRepeatCompile($element, $attr) {
// ngRepeat的表达式
var expression = $attr.ngRepeat;
var ngRepeatEndComment = document.createComment(' end ngRepeat: ' + expression + ' ');
// 省略
return function ngRepeatLink($scope, $element, $attr, ctrl, $transclude) {
if (trackByExpGetter) {
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);
};
}
var lastBlockMap = createMap();
// 监控集合属性变化
$scope.$watchCollection(rhs, function ngRepeatAction(collection) {
var index, length,
previousNode = $element[0], // 应该插入到previousNode节点之后
nextBlockMap = createMap(),
collectionLength,
key, value, // key/value of iteration
trackById,
trackByIdFn,
collectionKeys,
block, // last object information {scope, element, id}
nextBlockOrder,
elementsToRemove;
if (aliasAs) {
// 如果有 as 语法
$scope[aliasAs] = collection;
}
if (isArrayLike(collection)) {
// 类似于数组
collectionKeys = collection;
trackByIdFn = trackByIdExpFn || trackByIdArrayFn;
} else {
// 对象 但是要得到key组成的数组
trackByIdFn = trackByIdExpFn || trackByIdObjFn;
// if object, extract keys, in enumeration order, unsorted
collectionKeys = [];
for (var itemKey in collection) {
if (hasOwnProperty.call(collection, itemKey) && itemKey.charAt(0) !== '$') {
collectionKeys.push(itemKey);
}
}
}
collectionLength = collectionKeys.length;
nextBlockOrder = new Array(collectionLength);
// 定位现有的那些项
for (index = 0; index < collectionLength; index++) {
key = (collection === collectionKeys) ? index : collectionKeys[index];
value = collection[key];
trackById = trackByIdFn(key, value, index);
if (lastBlockMap[trackById]) {
// 如果存在就利用
block = lastBlockMap[trackById];
delete lastBlockMap[trackById];
nextBlockMap[trackById] = block;
nextBlockOrder[index] = block;
} else if (nextBlockMap[trackById]) {
// 现在的里边存在 说明 key 存在重复 报错
forEach(nextBlockOrder, function(block) {
if (block && block.scope) lastBlockMap[block.id] = block;
});
throw ngRepeatMinErr('dupes',
"Duplicates in a repeater are not allowed. Use 'track by' expression to specify unique keys. Repeater: {0}, Duplicate key: {1}, Duplicate value: {2}",
expression, trackById, value);
} else {
// 全新的项
nextBlockOrder[index] = {id: trackById, scope: undefined, clone: undefined};
nextBlockMap[trackById] = true;
}
}
// 移除剩余的哪些项 在新的里边不存在
for (var blockKey in lastBlockMap) {
block = lastBlockMap[blockKey];
elementsToRemove = getBlockNodes(block.clone);
$animate.leave(elementsToRemove);
if (elementsToRemove[0].parentNode) {
// if the element was not removed yet because of pending animation, mark it as deleted
// so that we can ignore it later
for (index = 0, length = elementsToRemove.length; index < length; index++) {
elementsToRemove[index][NG_REMOVED] = true;
}
}
// 销毁
block.scope.$destroy();
}
// 循环构建内容
for (index = 0; index < collectionLength; index++) {
key = (collection === collectionKeys) ? index : collectionKeys[index];
value = collection[key];
block = nextBlockOrder[index];
if (block.scope) {
// 以前就存在的
nextNode = previousNode;
// skip nodes that are already pending removal via leave animation
do {
nextNode = nextNode.nextSibling;
} while (nextNode && nextNode[NG_REMOVED]);
if (getBlockStart(block) != nextNode) {
// 移除掉
$animate.move(getBlockNodes(block.clone), null, jqLite(previousNode));
}
previousNode = getBlockEnd(block);
// 更新scope
updateScope(block.scope, index, valueIdentifier, value, keyIdentifier, key, collectionLength);
} else {
// 新的项 通过$transclude得到新的copy内容
$transclude(function ngRepeatTransclude(clone, scope) {
block.scope = scope;
// http://jsperf.com/clone-vs-createcomment
var endNode = ngRepeatEndComment.cloneNode(false);
clone[clone.length++] = endNode;
// 插入元素
$animate.enter(clone, null, jqLite(previousNode));
previousNode = endNode;
// 保留clone节点
block.clone = clone;
nextBlockMap[block.id] = block;
// 更新scope
updateScope(block.scope, index, valueIdentifier, value, keyIdentifier, key, collectionLength);
});
}
}
lastBlockMap = nextBlockMap;
});
};
}
};
}];
可以看出上边的基本过程,得到repeat的表达式,然后进行简单的解析,之后就返回了link函数;在link函数中主要是watch了集合的变化,发生改变时,如果有新增的项,那么就通过$transclude
得到新的clone内容,然后插入即可;如果是删除的话,直接也是移除了;而对于已存在的要去更新对应的scope信息。
核心还是利用$transclude
会每次克隆一份原始内容,而且本身会创建scope,所以就最大效用的发挥了其作用。