AngularJS原理篇
为了更好的解耦,各个框架(Extjs、jquery)的目标都是实现MVC,只是方式不同,而angular采用的是双向数据绑定。
图片摘自《用AngularJS开发下一代WEB应用》中文译者大漠穷秋的博客
如图,为了要实现双向数据绑定,angular以指令、scope、依赖注入、digest作为基础,而这些最终都是依赖于底层的JS版编译器。MVC、Service、factory、module上次都已经有介绍,那么这次我们先从compiler开始讲起。
一、compiler和directives
相关源码可见src/ng/compiler.js(本篇源码的版本号1.3.0-beta.11,下同)。
angular实现了一个JS版的编译器,如我们所知的编译器一样,会有编译(compiler)、链接(link)两个过程。
compiler过程主要包含:
1. 将所有指令按优先级排序(详见collectDirectives函数);
2. 执行每个指令的compile函数(详见applyDirectivesToNode函数),如果一个指令需要被克隆很多次(比如:ng-repeat),compile函数只在编译阶段被执行一次,复制这些模板,但是link函数会针对每个被复制的实例来执行;
3. 把每个compile函数返回的link函数打包到一个总的link函数中(详见compileNodes函数)
link过程主要包含:
1. 将scope绑定到DOM上;(详见nodeLinkFn函数)
2. 在元素上注册事件监听器;
3. 使用$watch监控数据模型,从而获知值的变化;
常见的一些JS Template框架也采用了类似的compile策略以提升效率,比如HandleBars、ExtJS的XTemplate。从这个角度来看,可以把angular的指令看作增强版的JS Template机制。
二、依赖注入(DI)
相关源码可见src/auto/injector.js。
熟悉Java的对依赖注入应该都不会陌生,Java实现依赖注入主要有三种方式:接口注入、set注入、构造注入,而angular采用的是构造注入。
代码分析:
98行-使用toString()来获得方法的定义
99行-正则表达式来查找方法的标志
100-102行-解析注入参数成array,并保存到$inject中
115行-返回参数集合
之所以有fn与array的区别是因为依赖注入有以下两种写法:
angular在获取到注入参数后,根据参数名称来获取对象实体,从而注入所需对象:
既然是根据参数名称,对于fn的解析方式在进行js代码压缩时,如果把$scope及$http压缩改名,则angular将找不到其对应的对象,会导致应用程序出错,而array的方式将能保留参数的字符串名称。
所以在定义你的服务时请尽量使用array的方式来定义参数。
三、双向数据绑定
相关源码可见src/ng/rootScope.js
双向数据绑定的核心问题是“脏值检测”——如何去判断值变化,难点是“循环依赖问题”——当值一直在变化(比如两个相互依赖的值发生改变),无法达到稳定状态(即值不变)时如何处理。
前面link过程中有说angular使用$watch来监控数据模型,以此来判断值的变化情况。我们先来看看$watch函数:
参数:
watchExp:需要监视的值或表达式
listener:监听函数,值变化时执行
objectEquality:是否开启值检测,为true时会检测对象或者数组内部变更(即选择以===的方式比较还是angular.equals的方式)
内部变量:
array:scope.$$watchers:存储注册过的所有监听器。每次digest时会去遍历array中所有监听器观察值的变化,直到值停止变更时才停止。
watcher:
last:监视对象的旧值,用于与现在的值进行比较来确定值是否改变。如果不相同,监听器就是dirty=true,它的监听函数就应当被调用。
返回值:
返回值是个函数,如果执行该函数,就会把刚注册的这个监听器销毁。
当监听到任何变化时,都会触发$digest循环:
$digest函数中会遍历所有监听器,并比对所有监听的值的变化。如果所有监听器的值都没变化,则dirty=false,不需要做任何更新;如果有任何监听器的值发生改变,则设置dirty=true,并修改对应值,$digest也会再次执行,直到所有的监听器值没有了改变。
注:在JavaScript里,NaN(Not-a-Number)并不等于自身,所以在脏检测函数里不显式处理NaN,因为一个值为NaN的监听器会一直是dirty。
那如何解决循环依赖呢?angular采用控制检测值变化的迭代次数:TTL,默认为10次。
如果超出迭代次数,值还不稳定,则抛出异常。
一般情况下开发者是不需要直接用$digest,angular提供的接口是$apply:
$apply使用函数作参数,它用$eval执行这个函数,然后通过$digest触发digest循环。而且$apply只有在创建一个不是angular库方法执行序列时才需要手动调用,因为angular的事件及异步请求等都会自动调用$apply。
注:在使用$apply时,请将要执行的事情包裹在$apply里面(即$scope.$apply(fn)的方式),因为如果要执行的事情出现异常,angular将无法捕获异常而导致出错,而$scope.$apply()已做了异常处理,并保证$digest()一定会被执行。
angular中还有一种延迟代码的方式,详见scope上的$evalAsync函数,它接受一个函数,把它列入计划,在当前正持续的digest中或者下一次digest之前执行。还实现了$$postDigest函数,将执行计划记录在$$postDigestQueue中,在digest之后运行。为了不影响到被列入计划将要执行的那个digest,angular的scope实现了一种叫做阶段(phase)的东西。
通过$watch循环检测,每次view或model改变时,该改变都能被捕捉到,通过$digest循环修改对应的model或view,直到稳定状态,从而实现双向数据绑定。
结语:
除了以上介绍的这些之外,angular还有很多东西值得我们去深入探究和使用,如路由(routing)、表单校验,还有单元测试和集成测试,有兴趣的朋友可以自己去探索一番。
最后再推荐一个学习资源集合:github上搜索AngularJS-Learning。
随着angular的发展,angular2.0已经在开发中。Angular2.0是一个针对移动应用的框架,同时也支持桌面环境。Angular2.0将基于ECMAScript6编写,有更快的更新检测(使用Object.observe()),更强大的功能(触摸动画、路由等),让我们一起期待。
转载于:https://my.oschina.net/yunuo/blog/294180