@frank-shaw
2016-09-24T16:27:50.000000Z
字数 5049
阅读 5325
angular.js
参考文章:
1. http://www.ifeenan.com/angularjs/2014-09-04-[%E8%AF%91]NG%E6%8C%87%E4%BB%A4%E4%B8%AD%E7%9A%84compile%E4%B8%8Elink%E5%87%BD%E6%95%B0%E8%A7%A3%E6%9E%90/
2. http://www.jvandemo.com/the-nitty-gritty-of-compile-and-link-functions-inside-angularjs-directives/
3. https://hairui219.gitbooks.io/learning_angular/content/zh/chapter05_5.html
对于一个新手而言,想要完全理解compile过程与link过程,确实很难。link过程又可分为pre-link与post-link过程。这里我疑惑的不仅仅是compile过程与link过程,同时对于compile link函数中的参数含义也不了解。所以需要查找很多资料,慢慢消化。
compile过程与link过程的具体区别可以直接通过下面的实例来感受一下:
//HTML文件
<!DOCTYPE html>
<html ng-app="plunker">
<head>
<meta charset="utf-8" />
<title>演示例子1</title>
<script>document.write('<base href="' + document.location + '" />');</script>
<link rel="stylesheet" href="style.css" />
<script data-require="angular.js@1.2.x" src="https://code.angularjs.org/1.2.22/angular.js" data-semver="1.2.22"></script>
<script src="app.js"></script>
</head>
<body>
<level-one>
<level-two>
<level-three>
Hello {{name}}
</level-three>
</level-two>
</level-one>
</body>
</html>
//app.js文件
var app = angular.module('plunker', []);
function createDirective(name){
return function(){
return {
restrict: 'E',
compile: function(tElem, tAttrs){
console.log(name + ': compile');
return {
pre: function(scope, iElem, iAttrs){
console.log(name + ': pre link');
},
post: function(scope, iElem, iAttrs){
console.log(name + ': post link');
}
}
}
}
}
}
app.directive('levelOne', createDirective('levelOne'));
app.directive('levelTwo', createDirective('levelTwo'));
app.directive('levelThree', createDirective('levelThree'));
结果输出如下图:
可以看到compile阶段与pre-link阶段是DOM结点层层深入的正向过程,post-link阶段是反向过程。为什么要这样子呢?
那么,我还想要知道,compile阶段与link阶段的区别是什么?而pre-link阶段与post-link阶段的区别又是什么?我们再来看看更加详细的这三者的输出区别,将app.js文件修改一下,打印不同阶段的element变量的原始html标记:
var app = angular.module('plunker', []);
function createDirective(name){
return function(){
return {
restrict: 'E',
compile: function(tElem, tAttrs){
console.log(name + ': compile => ' + tElem.html());
return {
pre: function(scope, iElem, iAttrs){
console.log(name + ': pre link => ' + iElem.html());
},
post: function(scope, iElem, iAttrs){
console.log(name + ': post link => ' + iElem.html());
}
}
}
}
}
}
app.directive('levelOne', createDirective('levelOne'));
app.directive('levelTwo', createDirective('levelTwo'));
app.directive('levelThree', createDirective('levelThree'));
结果如下图:
(对tElem.html()不理解?它实际上是一种借鉴jQuery直接对DOM元素操作的手法。请查看网页https://hairui219.gitbooks.io/learning_angular/content/zh/chapter05_5.html 以及API文档 https://docs.angularjs.org/api/ng/function/angular.element 的具体内容)
这个时候,我们就可以更加清晰地看到,compile阶段的DOM内容与link阶段的有了一定的区别。为什么会有这样的区别呢?
当浏览器渲染一个页面时,本质上是读html
标识,然后建立DOM
节点,当DOM
树创建完毕之后广播一个事件给我们。
当你在页面中使用script
标签加载ng
应用程序代码时,ng
监听上面的DOM
完成事件,查找带有ng-app
属性的元素。
当找到这样的元素之后,ng
开始处理DOM
以这个元素的起点,所以假如ng-app
被添加到html
元素上,则ng
就会从html
元素开始处理DOM
。
从这个起点开始,ng
开始递归查找所有子元素里面,符合应用程序里定义好的指令规则。
ng
怎样处理指令其实是依赖于指令被定义时的对象属性的,你可以定义一个compile
函数或者一个link
函数,或者用pre-link
和post-link
函数来代替link
函数。
我们已经知道当ng
发现DOM
构建完成时就开始处理DOM
。所以当ng在遍历DOM
的时候,碰到level-one
元素,从它的定义那里了解到,要执行一些必要的函数。
因为compile
函数定义在level-one
指令的指令对象里,所以它会被调用并传递一个element
对象作为它的参数。如果你仔细观察,就会看到,浏览器创建这个element
对象时,仍然是最原始的html标记。element
对象的理解可以参考:https://docs.angularjs.org/api/ng/function/angular.element。
在ng
中,原始DOM
通常用来标识template element
,所以在定义compile
函数参数时就用到了tElem名字,这个变量指向的就是template element
。一旦运行levelone
指令中的compile
函数,ng
就会递归深度遍历它的DOM
节点,然后在level-two
与level-three
上面重复这些操作。
即compile阶段处理的DOM
是原始的DOM
(template element
),compile阶段可以修改template element
。这是非常重要的一个认知。从参数列表中也可以看到,它并没有scope
这个参数,并没有与作用域绑定在一起。
compile阶段可以修改template element
,这也是其一个非常重要的应用:可以应用于当需要生成多个element实例,只有一个template element的情况。ng-repeat
内置指令就是一个最好的例子,它就在是compile
函数阶段改变原始的DOM
生成多个原始DOM
节点,然后每个又生成element
实例。因为这个阶段DOM
并没有与任何作用域数据进行绑定,意味着此时对DOM
进行操作只会有很少的性能开销。
深入了解pre-link
函数之前,让我们来看看post-link
函数。如果在定义指令的时候只使用了一个link
函数,那么ng
会把这个函数当成post-link
来处理,因此我们要先讨论这个函数。
当ng遍历完所有的dom并运行完所有的compile
函数之后,就反向调用相关联的post-link
函数。具体过程我们来看下图:
DOM
现在开始反向,并执行post-link
函数,在之前这种反向的调用看起来有点奇怪,其实这样做是非常有意义的:当运行包含子指令的指令post-link
时,反向的post-link
规则可以保证它的子指令的post-link
是已经运行过的。在我们的例子中,当运行level-one
指令的post-link
函数的时候,我们能够保证level-two
和level-three
的post-link
其实都已经运行过了。这就是为什么人们都认为post-link
是最安全或者默认的写业务逻辑的地方。
但是为什么这里的element
跟compile
相比为什么会有不同呢?从例子中我们已经看到了不同。
原因在于:一旦ng
调用过指令的compile
函数,就会创建一个template element
的element
实例对象,并且为它提供一个scope
对象。这个scope
有可能是新实例,也有可能是已经存在,可能是子scope
,也有可能是独立的scope
,这些都得依赖指令定义对象里的scope
属性值(相关内容可以查看:https://www.zybuluo.com/frank-shaw/note/482388)。
所以当linking
发生时,这个实例element
以及scope
对象已经是可用的了,并且被ng
作为参数传递到post-link
函数的参数列表中去。
关键点在于:post-link(pre-link)
函数的element
参数对象是一个element
实例而不是一个template element
,而且作用域与DOM
已经发生了绑定。所以你会看到例子中输出的不一样。
当写了一个post-link
函数,你可以保证在执行post-link
函数的时候,它的所有子级指令的post-link
函数是已经执行过的。在大部分的情况下,它都可以做的更好,因此通常我们都会使用它来编写指令代码。
然而,ng
为我们提供了一个附加的hook机制,那就是pre-link
函数,它能够保证在执行所有子指令的post-link
函数之前运行一些别的代码。
这句话是值得反复推敲的:pre-link
函数能够保证在element
实例上以及它的所有子指令的post-link
运行之前执行。
个人理解:pre-link
函数的存在主要是为了共享作用域scope
的变量的。假设一种情况:子指令的post-link
是在父指令的post-link
之前执行的,如果子指令的post-link
要用到父指令作用域的某些变量呢?这个时候pre-link
函数就可以派上用场了。这方面的理解还不够,以后添加吧。
下面是整个过程的流程图:
compile
与link
选项是互斥的。如果同时设置了这两个选项,那么会把compile
所返回的函数当做link
函数,而link
选项本身则会被忽略。这也就是为什么之前的例子中在compile
函数内部设置link
的原因。
两者的作用区分:compile
函数负责对template element
做一定的修改,而link
函数负责将作用域与DOM
进行连接,在link
内部可以设置事件监听器、监视数据变化和实时操作DOM
。