首先,linkFns数组用于存储每个DOM节点上所有指令的处理后的链接函数和子节点上所有指令的处理后的链接函数,具体使用递归的方式实现。随后,在返回的compositeLinkFn中,则是遍历linkFns,针对每个链接函数,创建起对应的作用域对象(针对创建隔离作用域的指令,创建隔离作用域对象,并保存在节点的缓存中),并处理指令是否设置了transclude属性,生成相关的transclude处理函数,最终执行链接函数;如果当前指令并没有链接函数,则调用其子元素的链接函数,完成当前元素的处理。
在具体的实现中,通过collectDirectives函数完成所有节点的指令扫描。它会根据节点的类型(元素节点,注释节点和文本节点)分别按特定规则处理,对于元素节点,默认存储当前元素的标签名为一个指令,同时扫描元素的属性和CSS class名,判断是否满足指令定义。
紧接着,执行applyDirectivesToNode函数,执行指令相关操作,并返回处理后的链接函数。由此可见,applyDirectivesToNode则是\$compile服务的核心,重中之重!
applyDirectivesToNode函数
applyDirectivesToNode函数过于复杂,因此只通过简单代码说明问题。
上文也提到,在该函数中执行用户定义指令的相关操作。
首先则是初始化相关属性,通过遍历节点的所有指令,针对每个指令,依次判断$$start属性,优先级,隔离作用域,控制器,transclude属性判断并编译其模板,构建元素的DOM结构,最终执行用户定义的compile函数,将生成的链接函数添加到preLinkFns和postLinkFns数组中,最终根据指令的terminal属性判断是否递归其子元素指令,完成相同的操作。
其中,针对指令的transclude处理则需特殊说明:
if (directive.transclude === 'element') { hasElementTranscludeDirective = true; var $originalCompileNode = $compileNode; $compileNode = attrs.$$element = $(document.createComment(' ' + directive.name + ': ' + attrs[directive.name] + ' ')); $originalCompileNode.replaceWith($compileNode); terminalPriority = directive.priority; childTranscludeFn = compile($originalCompileNode, terminalPriority); } else { var $transcludedNodes = $compileNode.clone().contents(); childTranscludeFn = compile($transcludedNodes); $compileNode.empty(); }
如果指令的transclude属性设置为字符串“element”时,则会用注释comment替换当前元素节点,再重新编译原先的DOM节点,而如果transclude设置为默认的true时,则会继续编译其子节点,并通过transcludeFn传递编译后的DOM对象,完成用户自定义的DOM处理。
在返回的nodeLinkFn中,根据用户指令的定义,如果指令带有隔离作用域,则创建一个隔离作用域,并在当前的dom节点上绑定ng-isolate-scope类名,同时将隔离作用域缓存到dom节点上;
接下来,如果dom节点上某个指令定义了控制器,则会调用\$cotroller服务,通过依赖注入的方式(\$injector.invoke)获取该控制器的实例,并缓存该控制器实例;
随后,调用initializeDirectiveBindings,完成隔离作用域属性的单向绑定(@),双向绑定(=)和函数的引用(&),针对隔离作用域的双向绑定模式(=)的实现,则是通过自定义的编译器完成简单Angular语法的编译,在指定作用域下获取表达式(标示符)的值,保存为lastValue,并通过设置parentValueFunction添加到当前作用域的$watch数组中,每次\$digest循环,判断双向绑定的属性是否变脏(dirty),完成值的同步。