通过上一篇我们知道,Angular在 bootstrap
后就开始编译整个文档了,12bet,使用的就是 Angular 里十分重要的服务-$compile
,指令的编译链接、双向数据绑定、12bet,各种监听等都是通过$compile
来完成的。
回看上一篇你可以知道,$compile
是在publishExternalAPI
时挂载在ng
模块下的服务,12bet,这就是$compile
服务的起源。
启动compile
$compile
真正派上用场就是在 bootstrap
创建完 injector
之后,也就是如下代码:
injector.invoke(['$rootScope', '$rootElement', '$compile', '$injector', '$animate',
function(scope, element, compile, injector, animate) {
scope.$apply(function() {
element.data('$injector', injector);
compile(element)(scope);
});
}
]);
上面的代码实际作用就是初始化相关的依赖,12bet,然后开始全局编译链接。12博体育,主要的代码就一行:
compile(element)(scope);
其实这是两步:
- compile(element):从根节点(
$rootElement
,即包含ng-app
的节点)开始,递归收集完整个页面的指令,12博体育,然后返回链接函数(publicLinkFn
) - publicLinkFn(scope):传入
$rootScope
,执行publicLinkFn
,完成整个页面的链接工作
compile(element)
先来看第一步的代码:
function compile($compileNodes, transcludeFn, maxPriority, ignoreDirective, previousCompileContext) {
// 对于文本节点,使用<span>包装
forEach($compileNodes, function(node, index) {
if (node.nodeType == NODE_TYPE_TEXT && node.nodeValue.match(/S+/)) {
$compileNodes[index] = jqLite(node).wrap('<span></span>').parent()[0];
}
});
// 调用compileNodes编译当前节点,返回链接函数
var compositeLinkFn =
compileNodes($compileNodes, transcludeFn, $compileNodes, maxPriority, ignoreDirective, previousCompileContext);
// 添加scope class
compile.$$addScopeClass($compileNodes);
var namespace = null;
return function publicLinkFn(scope, cloneConnectFn, options) {
// 后面再看
};
}
上面的代码的逻辑并不复杂,首先如果要查找的节点是文本元素,则包装一个span标签,然后执行compileNodes
函数,这个方法主要是收集指令,获得链接函数:
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();
// 收集每个节点上的所有指令
directives = collectDirectives(nodeList[i], [], attrs, i === 0 ? maxPriority : undefined, ignoreDirective);
// 应用指令,返回链接函数
nodeLinkFn = (directives.length)
? applyDirectivesToNode(directives, nodeList[i], attrs, transcludeFn, $rootElement, null, [], [], previousCompileContext)
: null;
// 添加scope class
if (nodeLinkFn && nodeLinkFn.scope) {
compile.$$addScopeClass(attrs.$$element);
}
// 如果父亲节点没有链接函数或者已终止(terminal)
// 或者没有孩子节点
// 孩子节点的链接函数就为null
// 否则递归编译孩子节点
childLinkFn = (nodeLinkFn && nodeLinkFn.terminal ||
!(childNodes = nodeList[i].childNodes) ||
!childNodes.length)
? null
: compileNodes(childNodes,
nodeLinkFn ? (
(nodeLinkFn.transcludeOnThisElement || !nodeLinkFn.templateOnThisElement)
&& nodeLinkFn.transclude) : transcludeFn);
// 将当前节点的链接函数和孩子节点的链接函数都插入到linkFns数组中
if (nodeLinkFn || childLinkFn) {
linkFns.push(i, nodeLinkFn, childLinkFn);
linkFnFound = true;
nodeLinkFnFound = nodeLinkFnFound || nodeLinkFn;
}
previousCompileContext = null;
}
// 如果有链接函数返回闭包(compositeLinkFn能访问linkFns)
return linkFnFound ? compositeLinkFn : null;
function compositeLinkFn(scope, nodeList, $rootElement, parentBoundTranscludeFn) {
// 代码略,后面再说
}
}
编译节点的主要流程为两步,第一步通过collectDirectives
搜集当前节点的指令,第二步调用applyDirectivesToNode
来应用指令,最后调用compileNodes
函数递归编译当前节点的子节点,最后把所有的函数添加到一个内部的linkFns
数组中,这个数组将在最后链接的时候用到。
收集指令
Angular收集指令是根据节点类型来的,一共有三种类型:
1.element node:先根据tagName使用addDirective
来添加指令,然后遍历节点的attrs使用addAttrInterpolateDirective
和addDirective
来添加指令,最后通过className使用addDirective
来添加指令
2.text node:调用addTextInterpolateDirective
方法来添加指令
3.comment node:匹配注释,使用addDirective
添加指令
收集指令的过程主要用到了三个方法:addDirective
、addAttrInterpolateDirective
和addTextInterpolateDirective
首先来看addDirective
方法:
function addDirective(tDirectives, name, location, maxPriority, ignoreDirective, startAttrName, endAttrName) {
// 是被忽略的指令,返回null
if (name === ignoreDirective) return null;
var match = null;
// hasDirectives系统在初始化的时候添加的一个内健指令对象集合,$injector
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数组中
tDirectives.push(directive);
match = directive;
}
} catch (e) { $exceptionHandler(e); }
}
}
return match;
}
对于属性值要处理是否修改和监听变化,例如{{name}}
就需要处理:
value="Hello, {{name}}"
对于属性的处理,使用的就是addAttrInterpolateDirective
函数了:
function addAttrInterpolateDirective(node, directives, value, name, allOrNothing) {
var trustedContext = getTrustedContext(node, name);
allOrNothing = ALL_OR_NOTHING_ATTRS[name] || allOrNothing;
// 处理html,返回篡改函数
var interpolateFn = $interpolate(value, true, trustedContext, allOrNothing);
// 没有需要监听处理的值 -> 忽略
if (!interpolateFn) return;
if (name === "multiple" && nodeName_(node) === "select") {
// 绑定到多个属性的节点时,报错
}
directives.push({
priority: 100,
compile: function() {
return {
pre: function attrInterpolatePreLinkFn(scope, element, attr) {
var $$observers = (attr.$$observers || (attr.$$observers = {}));
if (EVENT_HANDLER_ATTR_REGEXP.test(name)) {
// 不能篡改事件属性,报错
}
// 监听变化
var newValue = attr[name];
if (newValue !== value) {
interpolateFn = newValue && $interpolate(newValue, true, trustedContext, allOrNothing);
value = newValue;
}
if (!interpolateFn) return;
// 属性值初始化。例如{{name}} -> lwl
attr[name] = interpolateFn(scope);
($$observers[name] || ($$observers[name] = [])).$$inter = true;
(attr.$$observers && attr.$$observers[name].$$scope || scope).
$watch(interpolateFn, function interpolateFnWatchAction(newValue, oldValue) {
// 监听变化,设置属性值
if (name === 'class' && newValue != oldValue) {
attr.$updateClass(newValue, oldValue);
} else {
attr.$set(name, newValue);
}
});
}
};
}
});
}
如果是文本节点就创建内部指令,监听scope变化然后设置节点的值
function addTextInterpolateDirective(directives, text) {
var interpolateFn = $interpolate(text, true);
if (interpolateFn) {
directives.push({
priority: 0,
compile: function textInterpolateCompileFn(templateNode) {
var templateNodeParent = templateNode.parent(),
hasCompileParent = !!templateNodeParent.length;
if (hasCompileParent) compile.$$addBindingClass(templateNodeParent);
return function textInterpolateLinkFn(scope, node) {
var parent = node.parent();
if (!hasCompileParent) compile.$$addBindingClass(parent);
compile.$$addBindingInfo(parent, interpolateFn.expressions);
scope.$watch(interpolateFn, function interpolateFnWatchAction(value) {
node[0].nodeValue = value;
});
};
}
});
}
}
像如下的文本节点就会自动添加上面的指令,自动添加一个监听,通过原生方法来修改节点的值
<body>
{{ feenan }}
</body>
应用指令到当前节点
收集完所有指令之后,接下来就要调用applyDirectivesToNode
方法来将指令应用到当前节点上了,这个方法将会生成用于链接阶段的link函数。
这个函数比较复杂,我们拆开来看,其主体如下:
// 遍历当前节点的每个指令
for (var i = 0, ii = directives.length; i < ii; i++) {
// 1.判断scope类型
// 2.判断是否需要有controller
// 3.transclude的处理
// 4.template的处理
// 5.templateurl的处理
// 6.compile处理
// 7.terminal处理
}
// return 收集到的信息-nodeLinkFn
下面逐步来看看编译的完整过程,首先说明一下:当前节点$compileNode
-即要编译的节点
1.判断scope类型
if (directiveValue = directive.scope) {
// 跳过需要异步处理的指令,模板加载完成之后再处理
if (!directive.templateUrl) {
// scope属性为对象,例如{},则需要创建独立作用域
if (isObject(directiveValue)) {
newIsolateScopeDirective = directive;
}
}
newScopeDirective = newScopeDirective || directive;
}
2.判断是否需要controller
// 同样,跳过要异步处理的指令,模板加载完成之后再处理
if (!directive.templateUrl && directive.controller) {
directiveValue = directive.controller;
// 收集当前节点上所用要创建的controller
controllerDirectives = controllerDirectives || createMap();
controllerDirectives[directiveName] = directive;
}
3.transclude的处理
if (directiveValue = directive.transclude) {
hasTranscludeDirective = true;
// element
if (directiveValue == 'element') {
hasElementTranscludeDirective = true;
terminalPriority = directive.priority;
$template = $compileNode;
// 删除当前节点,替换为注释
$compileNode = templateAttrs.$$element =
jqLite(document.createComment(' ' + directiveName + ': ' + templateAttrs[directiveName] + ' '));
compileNode = $compileNode[0];
replaceWith(jqCollection, sliceArgs($template), compileNode);
// 编译当前节点
childTranscludeFn = compile($template, transcludeFn, terminalPriority, replaceDirective && replaceDirective.name, { nonTlbTranscludeDirective: nonTlbTranscludeDirective});
} else { // true
// 复制当前节点内容
$template = jqLite(jqLiteClone(compileNode)).contents();
// 清空当前节点
$compileNode.empty();
// 编译复制的内容
childTranscludeFn = compile($template, transcludeFn);
}
}
从上面的代码可以看出,transclude
的取值有两种:element和true,两者的区别是值为element时会将当前节点也处理了,而true时只处理当前节点的子节点。
4.template的处理
if (directive.template) {
hasTemplate = true;
templateDirective = directive;
// 如果template为函数,则函数返回值为模板内容
// 否则就是字符串,那么字符串就是模板内容
directiveValue = (isFunction(directive.template))
? directive.template($compileNode, templateAttrs)
: directive.template;
directiveValue = denormalizeTemplate(directiveValue);
// 如果replace为true
if (directive.replace) {
replaceDirective = directive;
if (jqLiteIsTextNode(directiveValue)) {
$template = [];
} else {
$template = removeComments(wrapTemplate(directive.templateNamespace, trim(directiveValue)));
}
// 模板的第一个节点
compileNode = $template[0];
if ($template.length != 1 || compileNode.nodeType !== NODE_TYPE_ELEMENT) {
// 没有要编译的内容或不是有效节点,报错
}
// 1.模板的第一个节点(compileNode)替换当前节点($compileNode)
replaceWith(jqCollection, $compileNode, compileNode);
var newTemplateAttrs = {$attr: {}};
// 2.收集模板的第一个节点(compileNode)的所有指令
var templateDirectives = collectDirectives(compileNode, [], newTemplateAttrs);
// 3.当前节点($compileNode)剩余未编译的指令
var unprocessedDirectives = directives.splice(i + 1, directives.length - (i + 1));
if (newIsolateScopeDirective) {
markDirectivesAsIsolate(templateDirectives);
}
// 4.$compileNode与compileNode的指令合并
directives = directives.concat(templateDirectives).concat(unprocessedDirectives);
// 5.将$compileNode与compileNode的属性合并
mergeTemplateAttributes(templateAttrs, newTemplateAttrs);
ii = directives.length;
} else {
// replace为false,直接将模板内容插入当前节点即可
$compileNode/(directiveValue);
}
}
5.templateurl的处理
if (directive.templateUrl) {
hasTemplate = true;
templateDirective = directive;
if (directive.replace) {
replaceDirective = directive;
}
// 和template的处理类似,只是在获取到模板之前,这些节点编译挂起
// 等获取到模板内容之后再继续编译
nodeLinkFn = compileTemplateUrl(directives.splice(i, directives.length - i), $compileNode,
templateAttrs, jqCollection, hasTranscludeDirective && childTranscludeFn, preLinkFns, postLinkFns, {
controllerDirectives: controllerDirectives,
newIsolateScopeDirective: newIsolateScopeDirective,
templateDirective: templateDirective,
nonTlbTranscludeDirective: nonTlbTranscludeDirective
});
ii = directives.length;
}
6.compile处理
// 同样,跳过需要异步处理的指令
if (!directive.templateUrl && directive.compile) {
try {
// 使用指令的compile函数编译指令
linkFn = directive.compile($compileNode, templateAttrs, childTranscludeFn);
// 如果返回的是函数,则该函数为post-link函数
// 否则返回为对象,pre和post属性分别对应指令的pre-link和post-link函数
if (isFunction(linkFn)) {
addLinkFns(null, linkFn, attrStart, attrEnd);
} else if (linkFn) {
addLinkFns(linkFn.pre, linkFn.post, attrStart, attrEnd);
}
} catch (e) {
$exceptionHandler(e, startingTag($compileNode));
}
}
7.terminal处理
// 有终止属性的指令,优先级小于当前指令的不会被编译
if (directive.terminal) {
nodeLinkFn.terminal = true;
terminalPriority = Math.max(terminalPriority, directive.priority);
}
编译阶段收集了足够的信息,最后返回nodeLinkFn
函数及其属性,在链接过程中就可以使用了:
nodeLinkFn = {
scope = newScopeDirective && newScopeDirective.scope === true
transcludeOnThisElement = hasTranscludeDirective
elementTranscludeOnThisElement = hasElementTranscludeDirective
templateOnThisElement = hasTemplate
transclude = childTranscludeFn
}
publicLinkFn(scope)
这里传进来的scope
是$rootScope
,这个方法主要是执行所有的链接函数,创建scope,添加监听函数:
function publicLinkFn(scope, cloneConnectFn, options) {
// 略去一大推
if (cloneConnectFn) cloneConnectFn($linkNode, scope);
if (compositeLinkFn) compositeLinkFn(scope, $linkNode, $linkNode, parentBoundTranscludeFn);
return $linkNode;
};
这个函数实际调用的是compositeLinkFn
函数,而compositeLinkFn
函数是compileNodes
函数的返回值,实际上它返回了一个闭包。compositeLinkFn
函数操作的是linkFns
,linkFns
包含两部分:当前节点的链接函数nodeLinkFn
和子节点的链接函数childLinkFn
,而childLinkFn
本身也是一个compositeLinkFn
(在子节点上递归调用compileNodes
的返回结果),所以实际的链接过程就是递归调用nodelinkFn
函数。
先来简单地看看外层compositeLinkFn
做了什么:
for (i = 0, ii = linkFns.length; i < ii;) {
node = stableNodeList[linkFns[i++]];
nodeLinkFn = linkFns[i++];
childLinkFn = linkFns[i++];
if (nodeLinkFn) {
if (nodeLinkFn.scope) {
// 新建子作用域
childScope = scope.$new();
} else {
// 使用父级作用域
childScope = scope;
}
// 包装一下TranscludeFn
if (nodeLinkFn.transcludeOnThisElement) {
// transclude: element
childBoundTranscludeFn = createBoundTranscludeFn(
scope, nodeLinkFn.transclude, parentBoundTranscludeFn,
nodeLinkFn.elementTranscludeOnThisElement);
} else if (!nodeLinkFn.templateOnThisElement && parentBoundTranscludeFn) {
childBoundTranscludeFn = parentBoundTranscludeFn;
} else if (!parentBoundTranscludeFn && transcludeFn) {
// transclude: true
childBoundTranscludeFn = createBoundTranscludeFn(scope, transcludeFn);
} else {
// 没有transclude属性
childBoundTranscludeFn = null;
}
// 实际链接处理就是nodeLinkFn了
nodeLinkFn(childLinkFn, childScope, node, $rootElement, childBoundTranscludeFn, nodeLinkFn);
} else if (childLinkFn) {
childLinkFn(scope, node.childNodes, undefined, parentBoundTranscludeFn)
}
}
nodeLinkFn
登场,看看链接都做了什么,要注意上面关于scope的处理,非独立作用域是在上面处理的,而独立作用域是在nodeLinkFn
中处理的。
如果设置了transclude
属性,都会调用createBoundTranscludeFn
方法,这个方法里会创建一个transcludedScope。
// 1.创建独立scope
if (newIsolateScopeDirective) {
isolateScope = scope.$new(true);
}
// 2.创建控制器
if (controllerDirectives) {
elementControllers = setupControllers($element, attrs, transcludeFn, controllerDirectives, isolateScope, scope);
for (i in elementControllers) {
controller = elementControllers[i];
var controllerResult = controller();
// 略
}
}
// 3.pre-link
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
);
}
// 4.递归执行子节点的链接函数
var scopeToChild = scope;
if (newIsolateScopeDirective && (newIsolateScopeDirective.template || newIsolateScopeDirective.templateUrl === null)) {
scopeToChild = isolateScope;
}
childLinkFn && childLinkFn(scopeToChild, linkNode.childNodes, undefined, boundTranscludeFn);
// 5.post-link
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
);
}
执行流程为preLinkFns
-> childLinkFn
-> postLinkFns
,这些信息都已经在第一步编译的时候收集好了,链接的时候使用即可。所以pre-link的执行顺序和文档节点顺序相同,而post-link是在递归回溯的过程中执行的,因此正好和文档节点顺序相反,后面会对编译连接过程给出一个小例子。
这里补充说一下如何获取节点上的所有控制器,看代码:
function setupControllers($element, attrs, transcludeFn, controllerDirectives, isolateScope, scope) {
var elementControllers = createMap();
for (var controllerKey in controllerDirectives) {
var directive = controllerDirectives[controllerKey];
var locals = {
$scope: directive === newIsolateScopeDirective || directive.$$isolateScope ? isolateScope : scope,
$element: $element,
$attrs: attrs,
$transclude: transcludeFn
};
var controller = directive.controller;
// 特殊处理ng-controller
if (controller == '@') {
controller = attrs[directive.name];
}
// 生成控制器实例,实际是返回已经注入依赖的工厂函数
var controllerInstance = $controller(controller, locals, true, directive.controllerAs);
// 含有 transclude 指令的元素是注释
// jQuery不支持在注释节点设置data
// 因此,暂时将controller设置在local hash中
// 当 transclude 完成后,生成真正的节点之后,再将controller设置到data中
elementControllers[directive.name] = controllerInstance;
if (!hasElementTranscludeDirective) {
$element.data('$' + directive.name + 'Controller', controllerInstance.instance);
}
}
return elementControllers;
}
例子
html结构如下
<body ng-app>
<A a1>
<B b1 b2></B>
<C>
<E e1></E>
<F>
<G></G>
</F>
</C>
<D d1></D>
</A>
</body>
js代码
var app = angular.module('myApp', []);
var names = ['a1', 'b1', 'b2', 'e1', 'd1'];
names.forEach(function (name) {
app.directive(name, function () {
return {
compile: function () {
console.log(name + ' compile');
return {
pre: function () {
console.log(name + ' preLink');
},
post: function () {
console.log(name + ' postLink');
}
};
}
};
});
});
控制台的输出:
// console log
a1 compile
b1 compile
b2 compile
e1 compile
d1 compile
a1 preLink
b1 preLink
b2 preLink
b2 postLink
b1 postLink
e1 preLink
e1 postLink
d1 preLink
d1 postLink
a1 postLink
可以看出:
- 所有的指令都是先compile,然后preLink,然后postLink。
- 节点指令的preLink是在所有子节点指令preLink,postLink之前,所以一般这里就可以通过scope给子节点传递一定的信息。
- 节点指令的postLink是在所有子节点指令preLink,postLink完毕之后,也就意味着,当父节点指令执行postLink时,子节点postLink已经都完成了,此时子dom树已经稳定,所以我们大部分dom操作,访问子节点都在这个阶段。
- 指令在link的过程,其实是一个深度优先遍历的过程,postLink的执行其实是一个回溯的过程。
- 节点上的可能有若干指令,在搜集的时候就会按一定顺序排列(通过Priority排序),执行的时候,preLinks是正序执行,而postLinks则是倒序执行。
来张图表示:
总结
编译链接过程,可以用下面的图简单的表示:
参考
- Directive Compilation in AngularJS – step-by-step
- AngularJS 源码分析4:$compile
- 彻底弄懂AngularJS中的transclusion