Angular.js源码分析之nginclude
还记得在源码分析之compile中在分析$CompileProvider
的实例directive
(也就是module实例可以用来自定义指令的directive
)方法的时候,里边说到对于每自定义一个指令其实都会有对应的Provider
存在,这里再次看下那部分代码:
/**
* 注册新的指令
*/
this.directive = function registerDirective(name, directiveFactory) {
assertNotHasOwnProperty(name, 'directive');
if (isString(name)) {
// key value 形式
assertValidDirectiveName(name);
assertArg(directiveFactory, 'directiveFactory');
if (!hasDirectives.hasOwnProperty(name)) {
// 还没有name的Directive工厂
hasDirectives[name] = [];
// 加后缀Directive
$provide.factory(name + Suffix, ['$injector', '$exceptionHandler',
function($injector, $exceptionHandler) {
// 此时使用的时候
// 取得所有的directiveFactory然后执行
// 得到的就是要如何构建指令的对象(指令对象)
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';
// 解析scope(bindToController)绑定
var bindings = directive.$$bindings =
parseDirectiveBindings(directive, directive.name);
if (isObject(bindings.isolateScope)) {
// 独立scope对象
directive.$$isolateBindings = bindings.isolateScope;
}
// 指定directive的$$moduleName
// 也就是在moduleInstance对象上暴露directive的时候使用的是
// invokeLaterAndSetModuleName 给directiveFactory赋值了$$moduleName
directive.$$moduleName = directiveFactory.$$moduleName;
directives.push(directive);
} catch (e) {
$exceptionHandler(e);
}
});
return directives;
}]);
}
// 添加
hasDirectives[name].push(directiveFactory);
} else {
// 批量注册 指令
forEach(name, reverseParams(registerDirective));
}
return this;
};
在那篇文章中有这样说:
从上边可以看出在调用
directive
的时候其实是会创建一个以name+Suffix
为名的service的(通过$provide.factory
的方法),也就意味着同一个名字的指令可以有多个directiveFactory
的,也就说我们可以一直增强同一个指令。
而在angular中一个指令被定义多次的就有这样的一个指令ngInclude
,也就是本篇要分析的重点。
先看定义部分:
$provide.provider('$compile', $CompileProvider).
directive({
// 省略
ngInclude: ngIncludeDirective,
// 省略
}).
directive({
ngInclude: ngIncludeFillContentDirective
})
第一次定义是用的ngIncludeDirective
,第二次是ngIncludeFillContentDirective
,下边就分别来分析分析他们。
ngIncludeDirective
看代码:
var ngIncludeDirective = ['$templateRequest', '$anchorScroll', '$animate',
function($templateRequest, $anchorScroll, $animate) {
return {
restrict: 'ECA',
priority: 400,
terminal: true,// 终止
transclude: 'element',
controller: angular.noop,
compile: function(element, attr) {
var srcExp = attr.ngInclude || attr.src,
onloadExp = attr.onload || '',
autoScrollExp = attr.autoscroll;
// link函数
return function(scope, $element, $attr, ctrl, $transclude) {
var changeCounter = 0,
currentScope,
previousElement,
currentElement;
// 清除上一个
var cleanupLastIncludeContent = function() {
if (previousElement) {
previousElement.remove();
previousElement = null;
}
if (currentScope) {
currentScope.$destroy();
currentScope = null;
}
if (currentElement) {
$animate.leave(currentElement).then(function() {
previousElement = null;
});
previousElement = currentElement;
currentElement = null;
}
};
// 监控其src的值 如果发生变化了需要重新请求得到内容
// 然后赋值给controller.template
scope.$watch(srcExp, function ngIncludeWatchAction(src) {
var afterAnimation = function() {
if (isDefined(autoScrollExp) && (!autoScrollExp || scope.$eval(autoScrollExp))) {
$anchorScroll();
}
};
var thisChangeId = ++changeCounter;
if (src) {
//set the 2nd param to true to ignore the template request error so that the inner
//contents and scope can be cleaned up.
$templateRequest(src, true).then(function(response) {
if (thisChangeId !== changeCounter) return;
var newScope = scope.$new();
ctrl.template = response;
// 注意没有对response的做其他处理了
// 真正的处理其实是在ngIncludeFillContentDirective里边
// 调用$transclude 传入新的scope
// 关键是这里调用了$transclude
var clone = $transclude(newScope, function(clone) {
cleanupLastIncludeContent();
$animate.enter(clone, null, $element).then(afterAnimation);
});
currentScope = newScope;
currentElement = clone;
currentScope.$emit('$includeContentLoaded', src);
scope.$eval(onloadExp);
}, function() {
if (thisChangeId === changeCounter) {
cleanupLastIncludeContent();
scope.$emit('$includeContentError', src);
}
});
scope.$emit('$includeContentRequested', src);
} else {
cleanupLastIncludeContent();
ctrl.template = null;
}
});
};
}
};
}];
这里先不说关键点,以及整个过程,先来看看ngIncludeFillContentDirective
大概定义。
ngIncludeFillContentDirective
看代码:
var ngIncludeFillContentDirective = ['$compile',
function($compile) {
return {
restrict: 'ECA',
priority: -400,
require: 'ngInclude',
link: function(scope, $element, $attr, ctrl) {
// 很简单 编译得到的内容 然后link到当然scope
if (/SVG/.test($element[0].toString())) {
// WebKit: https://bugs.webkit.org/show_bug.cgi?id=135698 --- SVG elements do not
// support innerHTML, so detect this here and try to generate the contents
// specially.
$element.empty();
$compile(jqLiteBuildFragment(ctrl.template, document).childNodes)(scope,
function namespaceAdaptedClone(clone) {
$element.append(clone);
}, {futureParentElement: $element});
return;
}
$element.html(ctrl.template);
$compile($element.contents())(scope);
}
};
}];
过程分析
OK,以及看过了两次定义,记个大概就好,下边来一段示例,具体分析下整个过程。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>angular ng-include demo</title>
<script type="text/javascript" src="angular.js"></script>
</head>
<body>
<div ng-app="app">
<div ng-include="url" class="includeEle"></div>
</div>
<script type="text/javascript">
var app = angular.module('app', []);
app.run(['$rootScope', function($rootScope) {
$rootScope.url = './include.html'
}])
</script>
</body>
</html>
假设include.html:
<p>include content</p>
其实很简单的demo,没有其他干扰。
-
页面load之后,初始化整个应用,而初始化的最后一步就是compile & link,先执行compile,返回的publicLinkFn1
-
compile从ng-app=”app”的元素(rootElement)开始,查看该div本身是没有指令的,但是他有childNodes,所以递归调用compileNodes(返回childLinkFn1,也就是指向的递归调用compileNodes的返回值compositeLinkFn2),返回compositeLinkFn1
1. 有效的childNodes(因为也有换行的文本节点也会被解析parse)就是includeEle,然后收集指令发现有ng-include,其实得到的结果是有两个指令directives的,名字都为ng-include,然后调用applyDirectivesToNode得到nodeLinkFn1
1. 遍历directives,发现第一个指令(也就是上边的`ngIncludeDirective`)是有transclude的,而且是'element',所以说为了得到childTranscludeFn1,需要递归调用compile(传入当前节点元素,同时也传入了当前指令的优先级priority,这样避免当前当前节点被重复应用当前的这个指令),返回的函数publicLinkFn2就是childTranscludeFn1
1. 依旧是类似的步骤(本质递归的过程):compileNodes,然后收集指令,由于传入了优先级priority,所以说这次收集得到的指令directives只有一个,也就是上边的`ngIncludeFillContentDirective`,应用指令(返回的是nodeLinkFn2),返回compositeLinkFn3
1. `ngIncludeDirective`带有terminal,所以就直接终止了
-
注意此时会给nodeLinkFn1设置属性transclude的值为childTranscludeFn1,也就是递归调用compile得到的publicLinkFn2
-
compile阶段依旧完成了,紧接着就是link了,然后会调用到rootElement返回的publicLinkFn1
-
publicLinkFn1会去调用compositeLinkFn1
1. 在compositeLinkFn1中会处理childLinkFn1,而childLinkFn1其实就是compositeLinkFn2,他会调用应用节点到指令`ngIncludeDirective`的返回的nodeLinkFn1
1. 在nodeLinkFn1中会执行指令中的preLink,childLinkFn(此时没有),然后postLinks(逻辑就在`ngIncludeDirective`定义中的,他通过$templateRequest,得到模板内容,然后调用$transclude)
1. nodeLinkFn1被调用的之前会得到一个boundTranscludeFn,__可以理解为__之前compile的时候的childTranscludeFn1,也就是publicLinkFn2,所以说在上一步$templateRequest的逻辑中调用的$transclude就会调用publicLinkFn2
1. publicLinkFn2的处理逻辑依旧类似:compositeLinkFn3 -> nodeLinkFn2 -> ngIncludeFillContentDirective的postLink函数:编译内容(`<p>include content</p>`),然后link
虽然说设置的场景已经摒弃了其他的感染,但是整个过程还是很复杂的,基本上都是一个递归处理的过程。
结语
虽然本篇要介绍的内容是关于ngInclude
这个指令的源码分析,但是其实这不是主要目的,主要目的还是利用这个特殊的指令来进一步帮助我们理解angular的compile和link。