angular的编译和渲染主要通过$CompileProvider
这个服务来实现的
一个简单的示例如下
{{msg}}
var app = angular.module('myApp', []);
app.directive("runoobDirective", function() {
return {
template : "自定义指令!
"
};
});
app.controller('myCtrl', function($scope) {
$scope.msg = "hello word";
});
在该示例中compile调用的结构图如下所示
有图可知compile1包括两个阶段
- compileNodes阶段, 该过程主要是解析元素各种指令进行指令的收集,然后生成link函数。
上述示例中可将{{{msg}}}
解析为{compile: textInterpolateLinkFn}
,然后push到当前元素的指令集中。 - compositeLinkFn阶段, 该过程主要执行上一阶段生成的link函数, 包括preLinkFns、postLinkFns的调用。
{{{msg}}}
生成的link函数会放入到postLinkFns中,用于页面渲染。
CompileProvider
该方法通过directive方法实现指令的注册, 返回compile作为后续编译渲染使用。hasDirectives用来缓存指令集工厂方法。
function $CompileProvider($provide, $$sanitizeUriProvider) {
var hasDirectives = {},
...
this.directive = function registerDirective(name, directiveFactory) {
if (!hasDirectives.hasOwnProperty(name)) {
hasDirectives[name] = [];
$provide.factory(name + Suffix, ['$injector', '$exceptionHandler',
function($injector, $exceptionHandler) {
var directives = [];
forEach(hasDirectives[name], function(directiveFactory, index) {
try {
var directive = $injector.invoke(directiveFactory);
if (isFunction(directive)) {
directive = { compile: valueFn(directive) };
} else if (!directive.compile && directive.link) {
directive.compile = valueFn(directive.link);
}
directive.priority = directive.priority || 0;
directive.index = index;
directive.name = directive.name || name;
directive.require = directive.require || (directive.controller && directive.name);
directive.restrict = directive.restrict || 'EA';
if (isObject(directive.scope)) {
directive.$$isolateBindings = parseIsolateBindings(directive.scope, directive.name);
}
directives.push(directive);
} catch (e) {
$exceptionHandler(e);
}
});
return directives;
}]);
}
hasDirectives[name].push(directiveFactory);
} else {
forEach(name, reverseParams(registerDirective));
}
return this;
};
this.$get = [
'$injector', '$interpolate', '$exceptionHandler', '$templateRequest', '$parse',
'$controller', '$rootScope', '$document', '$sce', '$animate', '$$sanitizeUri',
function($injector, $interpolate, $exceptionHandler, $templateRequest, $parse,
$controller, $rootScope, $document, $sce, $animate, $$sanitizeUri) {
return compile;
}
}
compile
compile作用上述已做说明
function compile($compileNodes, transcludeFn, maxPriority, ignoreDirective,
previousCompileContext) {
var compositeLinkFn =
compileNodes($compileNodes, transcludeFn, $compileNodes,
maxPriority, ignoreDirective, previousCompileContext);
....
return function publicLinkFn(scope, cloneConnectFn, options) {
$linkNode = $compileNodes;
if (transcludeControllers) {
for (var controllerName in transcludeControllers) {
$linkNode.data('$' + controllerName + 'Controller', transcludeControllers[controllerName].instance);
}
}
$linkNode = $compileNodes;
compile.$$addScopeInfo($linkNode, scope);
if (cloneConnectFn) cloneConnectFn($linkNode, scope);
if (compositeLinkFn) compositeLinkFn(scope, $linkNode, $linkNode, parentBoundTranscludeFn);
return $linkNode;
};
}
compileNodes
- 进行指令的收集工作
- 将指令作用到元素上
- 因为元素节点包括当前节点和子节点, 在生成link函数的过程中需要对这两种情况进行处理。
该方法在内部定义compositeLinkFn
用来将当前link和其后代的link进行组合, 定义一个数组linkFns用来存储生成的link函数,利用闭包的特性实现递归处理。
function compileNodes(nodeList, transcludeFn, $rootElement, maxPriority, ignoreDirective,
previousCompileContext) {
var linkFns = [],
attrs, directives, nodeLinkFn, childNodes, childLinkFn, linkFnFound, nodeLinkFnFound;
for (var i = 0; i < nodeList.length; i++) {
attrs = new Attributes();
// we must always refer to nodeList[i] since the nodes can be replaced underneath us.
directives = collectDirectives(nodeList[i], [], attrs, i === 0 ? maxPriority : undefined,
ignoreDirective);
nodeLinkFn = applyDirectivesToNode(directives, nodeList[i], attrs, transcludeFn, $rootElement,
null, [], [], previousCompileContext)
...
// 注意此处,对于children采用递归处理
childLinkFn = compileNodes(childNodes,
nodeLinkFn ? (
(nodeLinkFn.transcludeOnThisElement || !nodeLinkFn.templateOnThisElement)
&& nodeLinkFn.transclude) : transcludeFn);
inkFns.push(i, nodeLinkFn, childLinkFn);
//use the previous context only for the first element in the virtual group
previousCompileContext = null;
}
// return a linking function if we have found anything, null otherwise
return linkFnFound ? compositeLinkFn : null;
function compositeLinkFn(scope, nodeList, $rootElement, parentBoundTranscludeFn) {
var nodeLinkFn, childLinkFn, node, childScope, i, ii, idx, childBoundTranscludeFn;
var stableNodeList;
for (i = 0, ii = linkFns.length; i < ii;) {
node = stableNodeList[linkFns[i++]];
nodeLinkFn = linkFns[i++];
childLinkFn = linkFns[i++];
if (nodeLinkFn) {
nodeLinkFn(childLinkFn, childScope, node, $rootElement, childBoundTranscludeFn);
} else if (childLinkFn) {
childLinkFn(scope, node.childNodes, undefined, parentBoundTranscludeFn);
}
}
}
}
通过下图简单的说明link的包装结构
collectDirectives
进行指令收集
function collectDirectives(node, directives, attrs, maxPriority, ignoreDirective) {
var nodeType = node.nodeType,
switch (nodeType) {
case NODE_TYPE_ELEMENT: /* Element */
// use the node name:
addDirective(directives,
directiveNormalize(nodeName_(node)), 'E', maxPriority, ignoreDirective);
...
}
directives.sort(byPriority);
return directives;
}
applyDirectivesToNode
有代码可知带有模板的指令会在此处生成node的节点并挂载到当前节点上(通过指令来定义一个组件时需要有模板或者模板编译函数)。
执行directive.compile
方法生成linkFn
a. 如果linkFn的值是个方法则将其推入到 postLinkFns 中
b. 如果linkFn的值是一个对象,则分别将pre, post推入到preLinkFns、postLinkFns中。
function applyDirectivesToNode(directives, compileNode, templateAttrs, transcludeFn,
jqCollection, originalReplaceDirective, preLinkFns, postLinkFns,
previousCompileContext) {
...
// executes all directives on the current element
for (var i = 0, ii = directives.length; i < ii; i++) {
directive = directives[I];
...
if (directive.template) {
hasTemplate = true;
assertNoDuplicate('template', templateDirective, directive, $compileNode);
templateDirective = directive;
directiveValue = (isFunction(directive.template))
? directive.template($compileNode, templateAttrs)
: directive.template;
directiveValue = denormalizeTemplate(directiveValue);
if (directive.replace) {
...
} else {
$compileNode.html(directiveValue);
}
}
if (directive.templateUrl) {
...
} else if (directive.compile) {
try {
linkFn = directive.compile($compileNode, templateAttrs, childTranscludeFn);
if (isFunction(linkFn)) {
addLinkFns(null, linkFn, attrStart, attrEnd);
} else if (linkFn) {
addLinkFns(linkFn.pre, linkFn.post, attrStart, attrEnd);
}
} catch (e) {
$exceptionHandler(e, startingTag($compileNode));
}
}
...
}
nodeLinkFn.scope = newScopeDirective && newScopeDirective.scope === true;
nodeLinkFn.transcludeOnThisElement = hasTranscludeDirective;
nodeLinkFn.elementTranscludeOnThisElement = hasElementTranscludeDirective;
nodeLinkFn.templateOnThisElement = hasTemplate;
nodeLinkFn.transclude = childTranscludeFn;
previousCompileContext.hasElementTranscludeDirective = hasElementTranscludeDirective;
// might be normal or delayed nodeLinkFn depending on if templateUrl is present
return nodeLinkFn;
function nodeLinkFn(...) {
}
}
nodeLinkFn
该方法的主要功能, 执行controller方法、调用preLinkFns、递归调用子节点编译函数、postLinkFns
function nodeLinkFn(childLinkFn, scope, linkNode, $rootElement, boundTranscludeFn) {
var i, ii, linkFn, controller, isolateScope, elementControllers, transcludeFn, $element,
attrs;
if (compileNode === linkNode) {
attrs = templateAttrs;
$element = templateAttrs.$$element;
} else {
$element = jqLite(linkNode);
attrs = new Attributes($element, templateAttrs);
}
if (newIsolateScopeDirective) {
isolateScope = scope.$new(true);
}
if (boundTranscludeFn) {
// track `boundTranscludeFn` so it can be unwrapped if `transcludeFn`
// is later passed as `parentBoundTranscludeFn` to `publicLinkFn`
transcludeFn = controllersBoundTransclude;
transcludeFn.$$boundTransclude = boundTranscludeFn;
}
if (controllerDirectives) {
// TODO: merge `controllers` and `elementControllers` into single object.
controllers = {};
elementControllers = {};
forEach(controllerDirectives, function(directive) {
controllerInstance = $controller(controller, locals, true, directive.controllerAs);
elementControllers[directive.name] = controllerInstance;
controllers[directive.name] = controllerInstance;
});
}
if (controllers) {
forEach(controllers, function(controller) {
controller();
});
controllers = null;
}
// PRELINKING
for (i = 0, ii = preLinkFns.length; i < ii; i++) {
linkFn = preLinkFns[I];
invokeLinkFn(linkFn,
linkFn.isolateScope ? isolateScope : scope,
$element,
attrs,
linkFn.require && getControllers(linkFn.directiveName, linkFn.require, $element, elementControllers),
transcludeFn
);
}
// RECURSION
// We only pass the isolate scope, if the isolate directive has a template,
// otherwise the child elements do not belong to the isolate directive.
var scopeToChild = scope;
if (newIsolateScopeDirective && (newIsolateScopeDirective.template || newIsolateScopeDirective.templateUrl === null)) {
scopeToChild = isolateScope;
}
childLinkFn && childLinkFn(scopeToChild, linkNode.childNodes, undefined, boundTranscludeFn);
// POSTLINKING
for (i = postLinkFns.length - 1; i >= 0; i--) {
linkFn = postLinkFns[I];
invokeLinkFn(linkFn,
linkFn.isolateScope ? isolateScope : scope,
$element,
attrs,
linkFn.require && getControllers(linkFn.directiveName, linkFn.require, $element, elementControllers),
transcludeFn
);
}
function invokeLinkFn(linkFn, scope, $element, attrs, controllers, transcludeFn) {
try {
linkFn(scope, $element, attrs, controllers, transcludeFn);
} catch (e) {
$exceptionHandler(e, startingTag($element));
}
}
...
}
}
补充
@
设置属性的单项绑定、 =
双向绑定 &
事件的绑定
查看双向绑定可知, 设置lastValue属性来保存watch监听之前的父元素的值。 在watcher过程中会进行父子元素比较如果不同则说明有一个值发生变化。 如果此时的父元素与lastValue相同则说明子元素发生了变化,需要通知父元素进行变更;否则说明父元素发生变化,需重新设置子元素。
switch (mode) {
case '@':
attrs.$observe(attrName, function(value) {
isolateBindingContext[scopeName] = value;
});
attrs.$$observers[attrName].$$scope = scope;
if (attrs[attrName]) {
// If the attribute has been provided then we trigger an interpolation to ensure
// the value is there for use in the link fn
isolateBindingContext[scopeName] = $interpolate(attrs[attrName])(scope);
}
break;
case '=':
if (optional && !attrs[attrName]) {
return;
}
parentGet = $parse(attrs[attrName]);
if (parentGet.literal) {
compare = equals;
} else {
compare = function(a, b) { return a === b || (a !== a && b !== b); };
}
parentSet = parentGet.assign || function() {
// reset the change, or we will throw this exception on every $digest
lastValue = isolateBindingContext[scopeName] = parentGet(scope);
throw $compileMinErr('nonassign',
"Expression '{0}' used with directive '{1}' is non-assignable!",
attrs[attrName], newIsolateScopeDirective.name);
};
lastValue = isolateBindingContext[scopeName] = parentGet(scope);
var parentValueWatch = function parentValueWatch(parentValue) {
if (!compare(parentValue, isolateBindingContext[scopeName])) {
// we are out of sync and need to copy
if (!compare(parentValue, lastValue)) {
// parent changed and it has precedence
isolateBindingContext[scopeName] = parentValue;
} else {
// if the parent can be assigned then do so
parentSet(scope, parentValue = isolateBindingContext[scopeName]);
}
}
return lastValue = parentValue;
};
parentValueWatch.$stateful = true;
var unwatch;
if (definition.collection) {
unwatch = scope.$watchCollection(attrs[attrName], parentValueWatch);
} else {
unwatch = scope.$watch($parse(attrs[attrName], parentValueWatch), null, parentGet.literal);
}
isolateScope.$on('$destroy', unwatch);
break;
case '&':
parentGet = $parse(attrs[attrName]);
isolateBindingContext[scopeName] = function(locals) {
return parentGet(scope, locals);
};
break;
}
addDirective
function addDirective(tDirectives, name, location, maxPriority, ignoreDirective, startAttrName,
endAttrName) {
if (name === ignoreDirective) return null;
var match = null;
if (hasDirectives.hasOwnProperty(name)) {
for (var directive, directives = $injector.get(name + Suffix),
i = 0, ii = directives.length; i < ii; i++) {
try {
directive = directives[I];
if ((maxPriority === undefined || maxPriority > directive.priority) &&
directive.restrict.indexOf(location) != -1) {
if (startAttrName) {
directive = inherit(directive, {$$start: startAttrName, $$end: endAttrName});
}
tDirectives.push(directive);
match = directive;
}
} catch (e) { $exceptionHandler(e); }
}
}
return match;
}