由设计实现认识AngularJS

■ 文/ 天云软件 杨明翔

在谈论前端的框架、技术之前,先提一下设计模式。每个框架不是代码的累加,而是有一套贯穿灵魂的思想。之前初步学习Angular框架时是由项目入手,却没有完整化的学习其中的很多设计思想、实现原理,碎片化的知识积累反而延缓了前进的步伐,一路走来磕磕绊绊。今天就从设计原理、实现原理方面认识一下AngularJS的几大特征。

一、MVC模式

AngularJS是典型的MVC(model-view-controller)架构的衍生。在前端中,我们可以简单的理解为:

  • V-View,视图层,一般是我们的html文件层,用于展示数据内容。
  • C-Controller,控制器,帮助将M层数据给V,或者当View层数据有所改变时通知M层,M层数据也做相应的改变。
  • M-Model,模型,当我们需要使用到数据的时候,数据暂存在这里。如var vmData={name:’vm-2018’},或者从数据库中取出数据之后,将数据赋给一个对象或者变量,都可以理解为数据模型。

AJS01

传统MVC

在AngularJS的MVC中,Model作为控制器与视图之间传递数据的载体;View根据Model的设计而设计,用于显示Model;Controller用于根据Model进行不同的逻辑业务处理。

实际上,AngularJS经历了多次改版,早期版本更贴近于MVC,现在的话,如上述,已经与传统意义上的MVC模式有差别了。因此,所有称为MVC的衍生MVVM(Model-View-ViewModel)。

二、双向绑定

方向一:Model → View

{{Model数据}} 或<XXX ng-xxx=”Model数据”>  Model变View跟着变。

方向二:View → Model

<表单控件 ng-model=”Model数据名”>  View变Model跟着变。

当model改变时,更新响应的view。好处就是从model上解耦出view来减少依赖。但为什么model改变时会更新响应view呢?那么Angular是怎么做到的?那就得说一下观察者设计模式,其实这种设计模式在javascript本身事件里面就有诸多运用。实现原理是:model和view会设置一个观察者(watcher),如果model有改变就会触发一个事件去通知view,最终达到同步。具体在设计模式的指导下设计如下:

Angular有脏检查机制(dirty-checking)。dirty-checking(脏检测)原理简述:scope过$watch方法向this.$$watchers数组中添加watcher对象(包含watchFn, listenerFn, valueEq, last四个属性)。每当$digest循环被触发时,它会遍历$$watchers数组,行watcher中的watchFn,获取当前scope上某属性的值(一个watcher对应scope一个被监听属性),然后去同watcher中的last(上一次的值)做比较,若两值不相等,执行listenerFn。注意点ng只有在指定事件触发时,才进入$digest cycle:

① DOM事件,譬如用户输入文本,点击按钮等。(ng-click)

② XHR响应事件 ($http)

③ 浏览器Location变更事件 ($location)

④ Timer事件($timeout, $interval)

⑤ 执行$digest()或$apply()

AJS02

脏值检测过程

传统的JS MVC框架, 数据变更是通过setter去触发事件,然后立即更新UI。而Angular则是进入$digest cycle,等待所有model都稳定后,才批量一次性更新UI。这种机制能减少浏览器repaint次数,从而提高性能。

三、路由

提及MVC架构,路由是一个不可回避的特性,因为它是现在主流单页面应用(single page APP)的组成部分。为了实现无刷新的视图切换,我们通常会用ajax请求从后台取数据,然后套上HTML模板渲染在页面上。然而ajax的一个致命缺点就是导致浏览器后退按钮失效,尽管我们可以在页面上放一个大大的返回按钮,让用户点击返回来导航,但总是无法避免用户习惯性的点后退。解决此问题的一个方法是使用hash,监听hashchange事件来进行视图切换,另一个方法是用HTML5的history API,通过pushState()记录操作历史,监听popstate事件来进行视图切换,也有人把这叫pjax技术。AngularJS路由基本流程如下:

AJS03

简单说一下url中#(hash)的含义:hash 属性是一个可读可写的字符串,该字符串是URL的锚部分(从#号开始的部分)。

1. #的涵义

#代表网页中的一个位置。其右面的字符,就是该位置的标识符。比如,

http://www.ymx.com/index.html#print

就代表网页index.html的print位置。浏览器读取这个URL后,会自动将print位置滚动至可视区域。(单页应用)

2. HTTP请求不包括#

#是用来指导浏览器动作的,对服务器端完全无用。所以,HTTP请求中不包括#。在第一个#后面出现的任何字符,都会被浏览器解读为位置标识符。这意味着,这些字符都不会被发送到服务器端。比如,访问下面的网址,

http://www.ymx.com/index.html#print

浏览器实际发出的请求是这样的:

GET /index.html HTTP/1.1

Host: www.ymx.com

可以看到,只是请求index.html,根本没有”#print”的部分。

3. 单单改变#后的部分,浏览器只会滚动到相应位置,不会重新加载网页。

4. 每一次改变#后的部分,都会在浏览器的访问历史中增加一个记录,使用”后退”按钮,就可以回到上一个位置。

5. window.location.hash这个属性可读可写。读取时,可以用来判断网页状态是否改变;写入时,则会在不重载网页的前提下,创造一条访问历史记录。

6. HTML5新增事件onhashchange,当#值发生变化时,就会触发这个事件。IE 8+、Firefox 3.6+、Chrome 5+、Safari 4.0+支持该事件。以上浏览器url hash就能完成对单页面应用路由的支持。

四、依赖注入

谈论依赖注入之前,先说一下控制反转(IoC):

控制反转是针对面向对象设计不断复杂化而提出的一种设计原则,是利用面向对象编程法则来降低应用耦合的设计模式。IoC强调的是对代码引用的控制权由调用方转移到了外部容器,在运行是通过某种方式注入进来,实现了控制反转,这大大降低了程序之间的耦合度。依赖注入是最常用的一种实现IoC的方式,另一种是依赖查找。

Angular的依赖注入:

① 注入器(Injector):就像制造工厂,提供了一系列的接口,用于创建依赖对象的实例。

② 提供商(Provider):用于配置注入器,注入器通过它来创建被依赖对象的实例,Provider把令牌(Token)映射到工厂方法,被依赖的对象就是通过这个方法创建的。

③ 依赖(Denpendence):指定了被依赖对象的类型,注入器会根据此类型创建对应的对象。

AJS04

依赖注入实现过程

五、自定义指令

自定义指令的学习前首先要理解什么是指令。

指令是一个元素上的标记(例如属性、元素名称、注解或者CSS类)。指令指示AngularJS HTML模板绑定一个特定的逻辑行为到一个DOM元素上(例如,通过event监听器),甚至改变DOM元素或者他的子元素。

下面由一段代码来观测AngularJS由自定义指令名称开始 → 使用HTML模板 → 编写特定逻辑行为 → 链接到DOM元素这一过程。

AJS05

自定义指令代码及注释

小结

前端无论哪种框架、类库都是基于javascript语言、浏览器本身设计、HTML语言开发出来,学习一个新的框架、新的技术,它不是凭空诞生。我们追本溯源去理解它,方能快而准的切入框架本身并掌握它。