angular笔记分析.docx
- 文档编号:30753155
- 上传时间:2023-08-20
- 格式:DOCX
- 页数:48
- 大小:1.35MB
angular笔记分析.docx
《angular笔记分析.docx》由会员分享,可在线阅读,更多相关《angular笔记分析.docx(48页珍藏版)》请在冰豆网上搜索。
angular笔记分析
1、从开发者的角度,jqLite最明显的精简是不支持选择符。
你只能向angular.element传入一个DOM对象。
看起来AngularJS将这部分功能让位给浏览器原生的支持了,我们可以先使用浏览器的querySelector获得一个DOM对象:
如果某种原因你不愿意使用jqLite,也可以在AngularJS之前引入jQuery库。
AngularJS自动地将jqLite升级成jQuery,angular.element等同于$。
这样又可以使用熟悉的选择符了。
∙扩展的方法
jqLite包括一些额外的方法以适应AngularJS框架:
∙controller(name)-获得元素对应的控制器对象
∙injector()-获得元素对应的注入器对象
∙scope()-获得元素对应的作用域对象
∙isolateScope()-获得元素对应的隔离作用域对象,如果有的话。
∙inheritedData()-和data()一样,但如果当前元素没有指定的数据,会向上级节点继续找。
∙扩展的事件
jqLite还提供一个$destroy事件,当DOM对象被从DOM树删除时,AngularJS将触发这个事件,以便指令进行一些善后清理工作。
2、重写示例:
模板、指令和视图
AngularJS最显著的特点是用静态的HTML文档,就可以生成具有动态行为的页面。
还是前面的小时钟示例,我们使用AngularJS模板来重写,示例已经嵌入→_→:
HTML文件看起来像普通的HTML,只是其中多了一些特别的标记 (比如:
ng-app,ez-clock等等)。
在Angular中,这个HTML文件被称为模板。
ng-app这样的标记我们称之为指令。
模板通过指令指示AngularJS进行必要的操作。
比如:
ng-app指令用来通知AngularJS自动引导应用;ez-clock 指令用来通知AngularJS生成指定的时钟组件。
当AngularJS启动应用时,它会通过一个编译器解析处理这个模板文件,生成的结果就是:
视图:
我们定义了两个部件:
模板(包含指令的HTML文件)和指令实现 (JavaScript文件),AngularJS将这两部分拼装起来,生成了最终的视图。
使用指令封装JavaScript代码
我们在模板中使用了一个自定义的标签ez-clock,而它变成了一个会动的时钟,这期间发生了什么事情?
肯定不是浏览器干的,它不认识ez-clock是什么东西。
angular.min.js引入了基本的angularJS库,它会在浏览器载入HTML文档并且建立好DOM树后,执行以下操作:
1.找到有ng-app属性的DOM节点
2.以这个节点为根节点,搜索自定义指令,发现ez-clock
3.调用ez-clock指令的实现函数(指令类工厂)进行展开
根据我们的定义,ez-clock的展开操作如下:
1.使用一个div元素替换这个自定义标签
2.创建一个定时器,在定时器触发时刷新div元素的innerText
ez-clock这样的非HTML标准标签,在AngularJS中之所以称为指令/directive,就是指看到它时,基础框架需要对其进行解释,以便展开成浏览器可以理解的东西(HTML元素和脚本),而这个解释的过程被称为:
编译。
可见,AngularJS框架要求将HTML文档和JavaScript代码分割的更清晰,通常混杂在HTML文档中的JavaScript代码,需要以指令的形式进行封装,而模板、指令实现代码这两个部件,则由基础框架负责拼装运行。
将指令看做API
将DOM操作封装成指令,更容易进行分工与代码复用。
由于AngularJS更清晰地界定了一个WEB应用的组成部分,这样,在一个团队中,可以有人负责实现指令,有人负责开发模板,各自干擅长的事情,效率更高,成本更低。
当然,从编写界面HTML模板的角度看,诸如ez-clock之类的指令比div更具有语义性,使模板更容易维护,使指令的实现升级不影响模板,这也是不小的好处了。
与我们所熟悉的对象、函数这类接口完全不同,指令算是一种新型的API,它提供了在静态化的HTML文件中,植入动态行为的能力:
定义自己的指令
AngularJS内置的指令不能完全满足实际开发的需要,通常我们需要定义自己的指令:
∙增强标准DOM元素的行为
比如,希望一个DOM元素是可拖拽的,那么可以这样写:
1.
通过ez-draggable指令,我们赋予DOM元素可拖拽的能力。
∙自定义组件
比如,我希望一个图片裁剪功能,那么可以这样写:
1.
通过ez-photoshop指令,我们定义了一个包含交互行为的web组件。
∙封装其他组件库
这不是AngularJS鼓励的方向,但是确实有强劲的需求。
起点:
声明化
基于前面的示例,我们容易感受到使用AngularJS进行应用开发的一个重要的思维模式:
从构造声明式界面入手。
事实上,我猜测这也是Misko开发AngularJS最初的动机。
稍早一些的Flex、WPF和Firefox的XUL,或多或少给了Misko启发。
在使用AngularJS进行前端开发时,始终应该从构造声明式界面模板开始,如果现成的指令不够用,那么就定义自己的指令、实现自己的指令。
这是一个迭代的过程。
记住,指令是新型的API,用界面的声明化作为需求,来指导我们的代码封装。
3、作用域/Scope
将数据传递给指令
一个容易想到的方法,就是给指令ez-namecard增加一个属性,用这个属性的值指定数据对象的变量名称。
这相当于,用属性向指令(解释器)传递参数:
这样的话,ez-namecard的解释器只要检查data属性,然后执行一个eval()就可以获得sb的值,从而将其内容填充到展开的div片段中。
作用域/Scope
不过,请注意,前面定义的sb变量,默认是挂在window对象(命名空间)上的,即window.sb。
如果所有的数据都挂在window上,保不齐哪天就会出现变量的命名冲突。
AngularJS引入了一个自用的命名空间,也就是$rootScope对象,这样sb变量就可以挂在$rootScope上了,即$rootScope.sb。
引入了scope的代码参见→_→。
在HTML模板中,我们用了两个内置指令:
∙ng-app指令会在启动引导时创建一个$rootScope对象。
∙ng-init指令用来在作用域上初始化变量,这个指令将在$rootScope上建立sb对象。
在指令的实现代码中,与之前使用eval函数进行表达式估值不同,我们直接使用scope的$eval方法获得sb变量的值。
你可能注意到,这个scope是link函数传入的参数,它和我们说的$rootScope是一个东西吗?
?
?
?
层级的作用域
在AngularJS中,ng-app开始的DOM子树上,每个DOM对象都有一个对应的scope对象。
比如,在我们的示例中,body对象对应一个scope对象(因为body元素有ng-app属性,所以这个scope就是$rootScope对象),ez-namecard对象也对应一个scope对象....
在默认情况下,一个DOM子元素不会创建新的作用域,也就是说,这个子元素所对应的scope对象,其实就是它的最近一级的祖先对象对应的scope对象。
比如,在我们的例子中,ez-namecard对应的scope对象,就是它的父对象即body对象的scope对象,恰好也就是$rootScope对象;而ez-namecard有3个div子元素对应的scope对象,也是$rootScope对象。
有些指令会导致创建新的作用域,比如ng-controller。
如果在一个DOM对象上创建了新的作用域,那么这个scope对象的原型是其最近一级的组件对象的scope对象
比如在我们的例子中,如果在ez-namecard上使用ng-controller指令,那么ez-namecard对应的scope对象就不再是body对应的$rootScope对象,但是由于是原型继承,所以通过这个scope依然可以访问到sb变量。
4、监听数据的变化
我们已经实现了将数据显示到界面上,不过这还不够。
由于编译仅仅在启动引导时执行一次,这意味着我们的link函数只会被调用一次,那么,如果数据变化,在界面上将不会有任何反馈,即界面和数据将变得不同步了。
这需要持续监听数据的变化。
好在AngularJS的scope对象可以使用$watch()方法,对建立在其上的变量的变化进行监听:
$watch方法要求传入三个参数:
∙watchExpression-要监听的表达式,比如:
"sb"
∙listener-变化发生时的回调函数,AngularJS将向这个函数传入新值和旧值
∙objectEquality-如果监听表达式的值是一个对象,应当将这个参数置为true。
→_→是经过改进后的代码,当数据被改变时,界面会自动得到更新。
这时,我们称,建立了从数据到界面的单向绑定。
为了验证这一点,在代码中我们追加了一个定时器自动更新数据的age值。
5、如何修改数据
一旦在指令的实现代码中可以访问数据模型,那么使用声明式模板实现数据修改也非常简单了。
我们定义一个新的指令:
ez-namecard-editor,意图让其展开成这样:
在ez-namecard-editor的指令实现中,为了用input中的值自动更新sb变量中的值,我们需要在给input对象挂接上监听函数(示例中使用keyup事件),在监听函数中实现对sb变量的修改。
最终的效果是,用户在界面上进行的操作,自动地同步到了我们的数据。
这时,我们称,已经建立了从界面到数据的单向绑定。
ev.target.getAttribute获取的是field属性对应的值。
这样,当我们通过ez-namecard-editor修改数据时,可以同步地看到变化后的内容。
6、JSON.parse()和JSON.stringify()
parse用于从一个字符串中解析出json对象,如
json对象和对象是有一些不同的。
7、数据变化的传播
数据绑定有两个方向:
∙数据→界面:
我们使用scope对象的$watch()方法监听数据的变化,来更新界面。
∙界面→数据:
我们在界面的DOM对象上监听变化事件,来更新数据,并通过$apply()方法传播变化。
上面的图中,我们把ez-namecard和ez-namecard-editor都绑定到同一个sb对象上,那么在ez-namecard-editor上进行编辑,将导致sb对象发生变化;由于ez-namecard监听了这个变化,所以,ez-namecard的显示也应该变化。
∙$watch()
每个scope对象都维护了一个私有的监听队列,每次当我们在scope上执行一次$watch方法,就相当于向这个监听队列里塞入一个监听函数。
∙$apply()
为了捕捉对数据的修改,AngularJS要求开发者使用scope对象的$apply方法对数据进行修改,$apply方法内部会自动地调用监听队列里的监听函数,比如:
在有些情况下,AngularJS会自动调用$apply方法,比如在初次编译的时候。
但无论哪种情况,希望你能了解,对数据的变化监听,总是需要通过$apply方法的调用而被激活,如果AngularJS没有获得一个机会来调用$apply,就需要你手工的调用它。
这里面没有魔法。
什么时候需要调用$apply()要明确。
8、找不到的API?
AngularJS提供了一些功能的封装,但是当你试图通过全局对象angular去访问这些功能时,却发现与以往遇到的库大不相同。
∙$http
比如,在jQuery中,我们知道它的API通过一个全局对象:
$暴露出来,当你需要进行ajax调用时,使用$.ajax()就可以了。
这样的API很符合思维的预期。
AngularJS也暴露了一个全局对象:
angular,也对ajax调用进行封装提供了一个$http对象,但是,但是,当你试图沿用旧经验访问angular.$http时,发现不是那么回事!
仔细地查阅$http的文档,也找不到一点点的线索,从哪里可以把这个$http拿到。
∙依赖注入/DI
事实上,AngularJS把所有的功能组件都以依赖注入的方式组织起来:
在依赖注入的模式下,所有的组件必须通过容器才能相互访问,这导致了在AngularJS中,你必须通过一个中介才能获得某个组件的实例对象:
这个中介,就是依赖注入模式中的容器,在AngularJS中,被称为:
注入器。
在→_→的示例中,我们可以看到,我们已经拿到了$http对象,它其实是一个函数。
注入器/injector
注入器是AngularJS框架实现和应用开发的关键,这是一个DI/IoC容器的实现。
AngularJS将功能分成了不同类型的组件分别实现,这些组件有一个统称-供给者/provider,下图中列出了AngularJS几个常用的内置服务:
AngularJS的组件之间不可以互相直接调用,一个组件必须通过注入器才可以调用另一个组件。
这样的好处是组件之间相互解耦,对象的整个生命周期的管理甩给了注入器。
注入器实现了两个重要的功能:
1.集中存储所有provider的配方
配方其实就是:
名称+类构造函数。
AngularJS启动时,这些provider首先使用其配方在注入器内注册。
比如,http请求服务组件封装在$httpProvider类内,它通过"$http"这个名字在注入器内注册。
1.按需提供功能组件的实例
其他组件,比如一个用户的控制器,如果需要使用http功能,使用"$http"这个名字向注入器请求,就可以获得一个http服务实例了。
注册服务组件
从injector的角度看,组件就是一个功能提供者,因此被称为供给者/Provider。
在AngularJS中,provider以JavaScript类(构造函数)的形式封装。
服务名称通常使用一个字符串标识,比如"$http"代表http调用服务、"$rootScope"代表根作用域对象、"$compile"代表编译服务...
Provider类要求提供一个$get函数(类工厂),injector通过调用该函数,就可以获得服务组件的实例。
名称和类函数的组合信息,被称为配方。
injector中维护一个集中的配方库,用来按需创建不同的组件。
这个配方库,其实就是一个Hash对象,key就是服务名称,value就是类定义。
引导之前:
库阶段
我们说,框架还没有运转起来,现在还是库阶段。
只有通过启动引导,AngularJS框架才开始将那些组件拼接在一起,应用才真正开始运转。
像下面这样,试着给html元素增加一个ng-app指令,再重新运行!
自动引导启动框架
就像你看到的那样,如果HTML模板中有某个标签有ng-app属性,那么当DOM树建立成功后,AngularJS就会自动进入引导过程,启动整个框架:
手工引导启动框架
在大多数情况下,我们都使用ng-app指令来进行自动引导启动,但是如果一个HTML文件中有多个ng-app,AngularJS只会自动引导启动它找到的第一个ng-app应用,这是需要手工引导的一个应用场景。
我们可以利用angular.bootstrap()方法进行手动引导:
bootstrap方法有三个参数:
∙element:
一个DOM元素,以这个元素为Angular应用的根,等同自动引导时ng-app所在的元素。
这个参数是必须的。
比如:
document、document.body等。
∙modules:
引导时需要载入的模块数组。
比如:
[]、["ezstuff"]等。
由于我们的HTML中引用了ezstuff模块中定义的ez-duang指令,所以,我们需要指定载入ezstuff模块。
∙config:
引导配置项,可选。
我们先忽略。
引导第1步:
创建注入器
引导第3步:
编译DOM子树
引导过程的最后一步,是以ng-app所在DOM节点为根节点,对这棵DOM子树进行编译。
编译过程通常借助于指令,完成这几种操作:
1.对DOM对象进行变换。
2.在DOM对象上挂接事件监听。
3.在DOM对象对应的scope对象上挂接数据监听。
编译过程是AngularJS相当有特点的一个存在,我们将在下一节继续深入。
启动的底层的原理。
编译器/$compile
编译器$compile是一个AngularJS的内置服务,它负责遍历DOM树来查找匹配指令,并调用指令的实现代码进行处理。
HTML编译包括3个步骤:
∙匹配指令
$compile遍历DOM树,如果发现有元素匹配了某个指令,那么这个指令将被加入该DOM元素的指令列表中。
一个DOM元素可能匹配多个指令。
∙执行指令的编译函数
当一个DOM元素的所有指令都找齐后,编译器根据指令的优先级/priority指令进行排序。
每个指令的compile函数被依次执行。
每个compile执行的结果产生一个link函数,这些link函数合并成一个复合link函数。
∙执行生成的链接函数
$compile通过执行指令的link函数,将模板和scope链接起来。
结果就是一个DOM视图和scope对象模型之间的动态数据绑定。
为何将编译和连接两个步骤分开?
简单说,当数据模型的变化会导致DOM结构变化时,指令就需要分别定义compile()函数和link函数。
例如,ng-repeat指令需要为数据集合中的每个成员复制DOM元素。
将编译和链接过程分开可以有效地提高性能,因为DOM的复制放在compile()里,仅需要执行一次,但链接则发生在每个生成的DOM元素上,所以指令的link()函数会执行多次。
指令很少需要compile函数,因为大多数指令考虑的是作用于特定的DOM元素实例,而不是改变DOM的结构。
所以link函数更常用。
指令/directive
笼统地说,指令是DOM元素(例如属性、元素、CSS类等)上的标记符,用来告诉AngularJS的HTML编译器($compile服务)将特定的行为绑定到DOM元素,或者改变DOM元素。
指令可以放置在元素名、属性、CSS类名称及备注中。
下面是一些等效的触发"ng-bind"指令的写法:
指令的实现本质上就是一个类工厂,它返回一个指令定义对象,编译器根据这个指令定义对象进行操作。
指令的规范化
AngularJS在进行匹配检测之前,首先对HTML元素的标签和属性名转化成规范的驼峰式字符串:
1.去除名称前缀的x-和data-
2.以:
-或_为分割符,将字符串切分成单词,除第一个单词外,其余单词首字母大写
3.重新拼接各单词
控制器的作用
控制器的一次性
控制器构造函数仅在AngularJS对HTML文档进行编译时被执行一次。
从这个角度看,就更容易理解为何将控制器称为对scope对象的增强:
一旦控制器创建完毕,就意味着scope对象上的业务模型构造完毕,此后就不再需要控制器了-scope对象接管了一切。
控制器对scope的影响
ng-controller指令总是创建一个新的scope对象:
在图中,我们看到:
1.ng-app指令引发$rootScope对象的创建。
开始时,它是一个空对象。
2.body元素对应的scope对象还是$rootScope。
ng-init指令将sb对象挂在了$rootScope上。
3.div元素通过ng-controller指令创建了一个新的scope对象,这个对象的原型是$rootScope。
4.因为原型继承的关系,在do函数中对sb的引用指向$rootScope.sb。
初始化$scope对象
通常在应用启动时,需要初始化scope对象上的数据模型。
我们之前曾使用ng-init指令进行初始化,而使用控制器则是更为规范的做法。
右边的示例定义了一个ezController,利用这个控制器,我们对业务模型进行了初始化赋值:
请注意,控制器仅仅负责在编译时在scope对象上建立视图对象vm,视图对象和模板的绑定则是由scope负责管理的。
别把任何代码都塞到控制器里!
控制器的设计出发点是封装单个视图的业务逻辑,因此,不要进行以下操作:
∙DOM操作
应当将DOM操作使用指令/directive进行封装。
∙变换输出形式
应当使用过滤器/filter对输出显示进行转化。
∙跨控制器共享代码
对于需要复用的基础代码,应当使用服务/service进行封装
创建服务组件
在AngularJS中创建一个服务组件很简单,只需要定义一个具有$get方法的构造函数,然后使用模块的provider方法进行登记:
可配置的服务
有时我们希望服务在不同的场景下可以有不同的行为,这意味着服务可以进行配置。
比如,我们希望小计算器可以根据不同的本地化区域,给计算结果追加货币符号前缀,那么需要在这个服务创建之前,首先配置本地化区域的值,然后在具体的计算中,根据这个值选择合适的货币符号。
AngularJS使用模块的config()方法对服务进行配置,需要将实例化的服务提供者(而不是服务实例)注入到配置函数中:
注意:
服务提供者provider对象在注入器中的登记名称是:
服务名+Provider。
例如:
$http的服务提供者实例名称是"$httpProvider"。
服务定义语法糖
使用模块的provider方法定义服务组件,在有些场景下显得有些笨重。
AngularJS友好地提供了一些简化的定义方法,这些方法通常只是对provider方法的封装,分别适用于不同的应用场景:
∙factory
使用一个对象工厂函数定义服务,调用该工厂函数将返回服务实例。
∙service
使用一个类构造函数定义服务,通过new操作符将创建服务实例。
∙value
使用一个值定义服务,这个值就是服务实例。
∙constant
使用一个常量定义服务,这个常量就是服务实例。
factory方法
factory方法要求提供一个对象工厂,调用该类工厂将返回服务实例。
INSIDE:
AngularJS会将factory方法封装为provider,上面的示例等同于:
感受下和provider方法的区别!
直接就return和不需要$get了。
service方法
service方法要求提供一个构造函数,AngularJS使用这个构造函数创建服务实例:
需要提供一个方法。
INSIDE:
AngularJS会将service方法封装为provider,上面的示例等同于:
感受下和fa
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- angular 笔记 分析