本文从新人角度讲一讲对angular中MVVM模式的理解,以及angular特性的源码实现。
MVVM核心原理
MVVM模式是Model-View-ViewMode
(模型-视图-视图模型)模式的简称,其最早出现在微软的WPF和Silverlight框架中。MVVM模式利用框架内置的双向绑定技术对MVP(Model-View-Presenter)模式的变型,引入了专门的ViewModel(视图模型)来实现View和Model的粘合,让View和Model的进一步分离和解耦。
主要思想其实也很简单:在ViewModel中构建一组状态数据(state data),作为View状态的抽象。然后通过双向数据绑定(data binding)使ViewModel中的状态数据(state data)与View中的显示状态(screen state)保持一致。这样,ViewModel中的展示逻辑只需要修改对应的状态数据,就可以控制View的状态,从而避免在View上开发大量的接口。
MVVM模式的优势有如下四点:
- 低耦合:View可以独立于Model变化和修改,同一个ViewModel可以被多个View复用;并且可以做到View和Model的变化互不影响;
- 可重用性:可以把一些视图的逻辑放在ViewModel,让多个View复用;
- 独立开发:开发人员可以专注与业务逻辑和数据的开发(ViewModel),界面设计人员可以专注于UI(View)的设计;
- 可测试性:清晰的View分层,使得针对表现层业务逻辑的测试更容易,更简单。
angular中的MVVM模式
Igor Minar发布在Google+的文章中提到:
I’d rather see developers build kick-ass apps that are well-designed and follow separation of concerns, than see them waste time arguing about MV* nonsense. And for this reason, I hereby declare AngularJS to be MVW framework – Model-View-Whatever. Where Whatever stands for “whatever works for you”.
在文中特别指出angular在多次的API重构和改善,它越来越接近于MVVM模式,$scope可以被认为是ViewModel,而Controller则是装饰、加工处理这个ViewModel的JavaScript函数。作者更希望大家关注于实现一个成功的,具有好的设计以及遵循“分离关注点”原则的应用程序,而不是去争论MV*,所以他将angular称为MVW框架,是什么并不重要,只要适合你的应用就行。
下图是angular中关于MVVM模式的运用:
在angular中MVVM模式主要分为四部分:
- View:它专注于界面的显示和渲染,在angular中则是包含一堆声明式Directive的视图模板。
- ViewModel:它是View和Model的粘合体,负责View和Model的交互和协作,它负责给View提供显示的数据,以及提供了View中Command事件操作Model的途径;在angular中$scope对象充当了这个ViewModel的角色;
- Model:它是与应用程序的业务逻辑相关的数据的封装载体,它是业务领域的对象,Model并不关心会被如何显示或操作,所以模型也不会包含任何界面显示相关的逻辑。在web页面中,大部分Model都是来自Ajax的服务端返回数据或者是全局的配置对象;而angular中的service则是封装和处理这些与Model相关的业务逻辑的场所,这类的业务服务是可以被多个Controller或者其他service复用的领域服务。
- Controller:这并不是MVVM模式的核心元素,但它负责ViewModel对象的初始化,它将组合一个或者多个service来获取业务领域Model放在ViewModel对象上,使得应用界面在启动加载的时候达到一种可用的状态。
源码分析
AngularJS通过使用自己的事件处理循环,改变了传统的Javascript工作流。这使得Javascript的执行被分成原始部分和拥有AngularJS执行上下文的部分。只有在AngularJS执行上下文中运行的操作,才能享受到AngularJS提供的数据绑定,异常处理,资源管理等功能和服务。
angular中关于源码的理解可按下图来进行学习,这里只总结几个比较重要的特性实现。
$compile
在angular中,指令的编译链接、双向数据绑定、各种监听等都是通过$compile
来完成的。
$compile
是通过编译HTML字符串或者DOM到模版里,产生一个template function
,之后可以被用于scope
和template
的链接。
这个方法会遍历DOM并找到匹配的指令。一旦找到一个,它就会被加入一个指令列表中,这个列表是用来记录所有和当前DOM相关的指令的。 一旦所有的指令都被确定了,会按照优先级被排序,并且他们的compile方法会被调用。 指令的$compile()函数能修改DOM结构,并且要负责生成一个link函数。$compile方法最后返回一个合并起来的链接函数,这是链接函数是每一个指令的compile函数返回的链接函数的集合。
通过调用上一步所说的链接函数来将模板与作用域链接起来。这会轮流调用每一个指令的链接函数,让每一个指令都能对DOM注册监听事件,和建立对作用域的的监听。这样最后就形成了作用域的DOM的动态绑定。任何一个作用域的改变都会在DOM上体现出来。
1 | var $compile = ...; // injected into your code |
启动的方法在这里,只摘取关键代码.
1 | injector.invoke(['$rootScope', '$rootElement', '$compile', '$injector', '$animate', |
上面的代码主要作用就是,初始化相关的依赖,然后执行全局编译,最后更新所有的$watch.
核心的代码就这一句
1 | compile(element)(scope); |
其实这里有两步
compile(element)
收集完整个页面内的指令,然后返回publicLinkFn函数- 执行
publicLinkFn(scope)
此处的scope即为$rootScope
使用compile
函数可以改变原始的dom(template element),在ng创建原始dom实例以及创建scope实例之前。
可以应用于当需要生成多个element实例,只有一个template element的情况,ng-repeat就是一个最好的例子,它就在是compile函数阶段改变原始的dom生成多个原始dom节点,然后每个又生成element实例.因为compile只会运行一次,所以当你需要生成多个element实例的时候是可以提高性能的.
更多可以参考[译]ng指令中的compile与link函数解析
$digest
$watch
存储了监听函数,当作用域里的变量发生变化时,调用$digest
方法便会执行该作用域以及它的所有子作用域上的相关的监听函数,从而做一些操作(如:改变view)。
不过一般情况下,我们不需要手动调用$digest
或者$apply
(如果一定需要手动调用的话,我们通常使用$apply
,因为它里面除了调用$digest
还做了异常处理),因为内置的directive
和controller
内部(即Angular Context之内)都已经做了$apply
操作,只有在Angular Context之外的情况需要手动触发$digest
,如: 使用setTimout修改scope(这种情况我们除了手动调用$digest
,更推荐使用$timeout
服务,因为它内部会帮我们调用$apply
)。
digest
方法是dirty check
的核心,也是双向绑定的主要实现,主要思路是先执行$$asyncQueue
队列中的表达式,然后开启一个loop
来的执行所有的watch
里的监听函数,前提是前后两次的值是否不相等,假如ttl
超过系统默认值,则dirty check
结束,最后执行$$postDigestQueue
队列里的表达式。
1 |
|
通过上面的代码,可以看出,核心就是两个loop,外loop保证所有的model都能检测到,内loop则是真实的检测每个watch,watch.get就是计算监控表达式的值,这个用来跟旧值进行对比,假如不相等,则执行监听函数
注意这里的watch.eq这是是否深度检查的标识,equals方法是angular.js里的公共方法,用来深度对比两个对象,这里的不相等有一个例外,那就是NaN ===NaN,因为这个永远都是false,所以这里加了检查。
另外:$RootScopeProvider
中提供了digestTtl
方法,用于修改TTL的值(默认是10),可以这样修改:
1 | angular.module('ng').config(['$rootScopeProvider', function ($RootScopeProvider) { |
isolate scope
Isolate标识来创建独立作用域,这个在创建指令并且scope属性定义的情况下,会触发这种情况,还有几种别的特殊情况,如果是独立作用域的话,会多一个$root属性,这个默认是指向rootscope的
如果不是独立的作用域,则会生成一个内部的构造函数,把此构造函数的prototype指向当前scope实例
$injector
依赖注入
每一个AngularJS应用都有一个注入器(injector)用来处理依赖的创建。注入器是一个负责查找和创建依赖的服务定位器。
1 | var FN_ARGS = /^function\s*[^\(]*\(\s*([^\)]*)\)/m; |
annotate函数通过对入参进行针对性分析,若传递的是一个函数,则依赖模块作为入参传递,此时可通过序列化函数进行正则匹配,获取依赖模块的名称并存入$inject数组中返回,另外,通过函数入参传递依赖的方式在严格模式下执行会抛出异常;第二种依赖传递则是通过数组的方式,数组的最后一个元素是需要使用依赖的函数。annotate函数最终返回解析的依赖名称。
Angular优缺点及应用场景
angular功能全,利用它开发效率可以得到提高,有庞大的社区支持,没有内存泄露隐患,但是在性能上dirty check
算是拖了后腿。
angular适合构建CRUD应用,因为它具有构建一个CRUD应用时可能用到的所有技术:数据绑定、基本模板指令、表单验证、路由、深度链接、组件重用、依赖注入。对于像游戏和有图形界面的编辑器之类的应用,会进行频繁且复杂的DOM操作,和CRUD应用不同。因此,可能不适合用Angular来构建。在这种场景下,使用更低抽象层次的类库可能会更好。
参考:
中文API: