[关闭]
@Belinda 2015-05-15T03:17:25.000000Z 字数 8962 阅读 1714

angular js(v2.0)

学习笔记


jQuery时代

托互联网日新月异发展的福,浏览器变成了人们接入互联网的入口,而JavaScript 这个超级丑小鸭,在21世纪第一个十年后期,随着google一系列WEB应用的流行, 终于成功地站到了舞台的中央,唤起了开发者对JavaScript的兴趣。

浏览器里原生的JavaScript有点像汇编语言,不同的浏览器就像不同的CPU架构, 汇编语言各有千秋,这让前端开发者很恼火。聪明人很快发现了这个痛点,于是, 抹平浏览器差异的jQuery出现了。

jQuery由一小撮对浏览器极其熟稔的极客负责抹平不同浏览器的差异,其他开发 者只需要基于jQuery进行开发,可以更好地关注业务实现,而不是把时间花在 适配不同的浏览器上。

这样的分工符合经济学原理,开启了一个不可忽视的jQuery时代。

前端开发的jQuery范式

jQuery使得开发无刷新动态页面变得相当简单。

标准的HTML页面是静态的,被浏览器渲染后就产生了一个DOM树。jQuery提供了 一系列的选择符让开发者能够极其方便地选中一组DOM节点,对其进行操作或者 挂接事件监听函数。

这就是jQuery的开发范式,除了语法糖,没有引入什么新的概念,只是朴素地, 让你能够更简单地、低成本地操作DOM:

用选择符选定一组DOM节点
操作这些DOM节点,比如:修改文本、改变属性、挂接事件监听函数等等。
基本不用考虑跨浏览器的兼容性
jQuery的API符合大多数开发者的预期,因此,很容易上手。

示例:用jQuery实现一个小时钟

我们试着用jQuery实现一个简单的时钟页面,实现思路很简单:

  1. 引入jquery库
  2. 在DOM文档就绪后,启动一个1秒1次的定时器
  3. 在定时器每次触发时,取当前时间值,更新div#clock的文本
    右侧的编辑器中预置了实现代码,你应该已经看到运行效果了:

    <html>
    <head>
        <script src="http://libs.baidu.com/jquery/1.11.1/jquery.min.js"></script>
    </head>
    <body>
        <div id="clock"></div>
    </body>
    </html>
    
    
    <script>
    $(function(){
    setInterval(function(){
    var d = new Date();
    $("#clock").text(d.toString());
    },1000);
    });
    
    </script>
    

永远有人追求进步

jQuery有点像C语言,威力很大,不过要弄出点像样的前端界面,还得花不少功夫 处理琐碎的事情。

还能再简单些吗?Misko Hevery认为可以。于是,AngularJS诞生了。

AngularJS引入了三个主要的概念,期望让前端开发更系统化一些:

  1. 声明式界面开发
  2. 双向数据绑定
  3. 使用依赖注入解耦

很多人在初次接触AngularJS时,都有些吃惊,因为它把前端开发搞的突然严肃起来 了。考虑到Misko曾经是一个Java程序员,这一切就好理解了。

Java程序员擅长引入复杂的架构来解决简单的问题。不是吗?

重写小时钟示例

AngularJS最大的卖点是用静态的HTML文档,就可以定义具有动态行为的页面。

还是前面的小时钟示例,我们使用AngularJS模板来重写,示例已经嵌入在编辑器中→_→:

先看HTML文件。请使用鼠标轻击右边编辑器的HTML按钮,切换到HTML编辑器。

    <html>
    <head>
        <script src="http://lib.sinaapp.com/js/angular.js/angular-1.2.19/angular.min.js"></script>
    </head>
    <body ng-app="ezstuff">
        <ez-clock></ez-clock>
    </body>
    </html>

HTML文件看起来像普通的HTML,只是其中多了一些特别的标记(比如:ng-app,ez-clock等等)。 在Angular中,这个HTML文件被称为模板。

第一种特别的标记我们称之为指令。指令可以为HTML元素添加额外的行为(让HTML 动起来)。在这个例子中,我们使用了一个ng-app指令,这个指令用来通知Angular自动 引导应用(晚点会解释这个“引导”);我们还使用了一个自定义的ez-clock指令, 这个指令是我们自己实现的,用来产生那个小时钟。

再切换到JavaScript编辑器查看一下JavaScript代码。

    angular.module("ezstuff",[])
    .directive("ezClock",function(){
        return {
            restrict : "E",
            template : "<div></div>",
            link : function(scope,element,attrs){
                setInterval(function(){
                    var d = new Date();
                    element.text(d.toString());
                },1000);
            }
        }
    })

JavaScript代码就是ez-clock指令的实现,我们先不深究它的写法,但请注意下,在代码中 真正干活的是setInterval(...)那个调用。

当Angular启动应用时,它会通过一个编译器解析、处理这个模板文件,生成的结果就是:视图。

思考一下

我们在模板中指定了一个自定义的标签ez-clock,而它变成了一个会动的时钟。 这中间发生了什么?

浏览器不会理解ez-clock这个标签,是脚本做了这个工作。

angular.min.js引入了基本的angularJS框架,它会在浏览器载入HTML文档并且 建立好DOM树后,执行以下操作:

  1. 找到有ng-app属性的DOM节点
  2. 以这个节点为根节点,重新解释DOM树,具体说就是子树
  3. 在解释过程中,发现ez-clock这个指令
  4. 调用ez-clock指令的实现进行展开

ez-clock的展开操作如下:

  1. 使用一个div元素替换这个自定义标签
  2. 创建一个定时器,在定时器触发时刷新div元素的innerText

ez-clock这个自定义的标签,在AngularJS中北称为指令/directive,意思是 看到这个指令,AngularJS基础框架需要对其进行解释,以便展开成浏览器可以理解 的东西(HTML元素和脚本),而这个解释的过程,有一个特定的名称:编译。

可见,要写的代码一点也不会少,只是,代码被一种新的方式重新组织了。

好处在哪里?

答案是在开发过程中,便于分工与代码复用。

在小的项目中也可以应用AngularJS,这样你可以得到思维的锻炼。但是真正发生 威力,是在一个团队中,可以有专人负责实现指令(比如:ez-clock),其他人只需要 利用这些指令编写模板,这样的成本更低。

符合经济学原理,不是吗?

当然,从编写界面HTML模板的角度看,ez-clock比div更具有语义性,使模板更容易维护, 使指令的实现升级不影响模板,这也是不小的好处了。

指令算是新型的API,与我们所熟悉的对象、函数这类接口完全不同,它提供了在 静态化的HTML文件中,指定动态行为的能力。

起点:声明化

基于前面的示例,我们容易感受到使用AngularJS开发的一个范式:从构造声明式界面模板入手。

事实上,我猜测这也是Misko开发AngularJS最初的动机。稍早一些的Flex、WPF和Firefox 的XUL,应该或多或少给了Misko启发。

在使用AngularJS进行前端开发时,始终应该从构造声明式模板开始,如果现成的指令不够 用,那么就定义自己的指令、实现自己的指令。这是一个迭代的过程。

记住,指令是新型的API,用指令来指导我们的代码封装。

抽取自己的指令

我们很容易从开发过程中出抽象出来一些指令,这些指令的实现我们留待后续环节完成。

增强标准DOM元素的行为
比如,我希望一个DOM元素是可拖拽的,那么可以这样写:

    <p ez-draggable="true">...</p>

我们用ez-draggable指令赋予DOM元素可拖拽的能力。

自定义组件
比如,我希望一个图片裁剪功能,那么可以这样写:

    <ez-photoshop src="a.jpg"></ez-photoshop>

我们用ez-photoshop指令定义了一个包含交互行为的web组件。

在声明式模板中显示数据

因为不能像jQuery一样将DOM操作混在模板里,声明式模板的范式很快让我们变得束手束脚。

一个典型的问题:在声明式模板里怎么显示数据?

假设我们有某人的基本信息,保存在一个json对象里:

    var sb = {
        name : 'somebody',
        gender : 'male',
        age : 28
    };

我们定义一个指令ez-namecard,希望它经过编译后会展开成这样:

    <div>
        <div>name : somebody </div>
        <div>gender : male </div>
        <div>age : 28</div>
    </div>

那么,怎么把sb这个json对象指定给ez-namecard这个指令?

将数据传递给指令

一个很容易想到的定义方法,就是给指令ez-namecard增加一个属性,用这个属性的值 指明数据对象。

这相当于,用属性向指令(解释器)传递参数:

    <ez-namecard data="window.sb"></ez-namecard>

这样的话,ez-namecard的解释器只要检查data属性,然后执行一个eval()就可以获得sb的值, 从而将其内容填充到展开的div片段中。

BINGO!

作用域/Scope

不过,请注意,前面定义的sb变量,默认是挂在window对象上的,即window.sb。如果所有的 数据都挂在window上,保不齐哪天就会出现变量的命名冲突,所以,AngularJS引入了一个自用 的命名空间,也就是rootScopesbrootScope上了,即$rootScope.sb。

引入了scope的代码参见→_→。

我们先看下HTML的变化。

    <html>
    <head>
        <script src="http://lib.sinaapp.com/js/angular.js/angular-1.2.19/angular.min.js"></script>
    </head>
    <body ng-app="ezstuff" ng-init="sb = {name:'somebody',gender:'male',age:28}">
        <ez-namecard data="sb"></ez-namecard>
    </body>
    </html>

ng-app指令会指示AngularJS基础框架在启动引导时创建一个rootScopenginitsbrootScope上。

再看JavaScript代码的变化。

    angular.module("ezstuff",[])
    .directive("ezNamecard",function($rootScope){
        return {
            restrict : "E",
            template : "<div></div>",
            link : function(scope,element,attrs){
                var sb = $rootScope.$eval(attrs.data);
                element.append("<div>name : " + sb.name + "</div>")
                    .append("<div>gender : " + sb.gender + "</div>")
                    .append("<div>age : " + sb.age + "</div>")
            }
        };
    });

与之前使用eval函数进行表达式估值不同,这里我们直接使用rootScopeeval方法获得在 $rootScope上sb变量的值。

再仔细看一下,$rootScope是我们在定义ezNamecard这个指令,通过函数参数表的方式注入的 (关于注入,我们稍后讲解,现在只需要知道,这些函数在适当的时候会被AngularJS框架调用), 而link函数,也有一个参数scope。

这两个有什么区别?

层级的作用域

在AngularJS中,ng-app开始的DOM子树上,每个DOM对象都有一个对应的scope对象。 比如,在我们的示例中,body对象对应一个scope对象(因为body元素有ng-app属性,所以 这个scope就是$rootScope对象),ez-namecard对象也对应一个scope对象...

在默认情况下,一个DOM子元素不会创建新的作用域,也就是说,这个子元素所对应的 scope对象,其实就是它的最近一级的祖先对象对应的scope对象。比如,在我们的例子中, 我们没有在ez-namecard创建新的作用域,那么它对应的scope对象,就是它的父对象即 body对象的scope对象,恰好也就是rootScopeeznamecard3divdivscopebodyrootScope对象。

有些指令会导致创建新的作用域,比如ng-controller。如果在一个DOM对象上创建了新 的作用域,那么这个scope对象的原型是其最近一级的组件对象的scope对象。比如在我们 的例子中,如果在ez-namecard上使用ng-controller指令,那么ez-namecard对应的scope 对象就不再是body对应的$rootScope对象,但是由于是原型继承,所以通过这个scope依然 可以访问到sb变量。

监听数据的变化

我们已经实现了将数据显示到界面上,不过这还不够。由于编译仅仅在启动引导时执行一次, 这意味着我们的link函数只会被调用一次,那么,如果数据变化了,在界面上将不会有任何 反馈,即界面和数据将变得不同步了。

这需要持续监听数据的变化。

    <html>
    <head>
        <script src="http://lib.sinaapp.com/js/angular.js/angular-1.2.19/angular.min.js"></script>
    </head>
    <body ng-app="ezstuff" ng-init="sb = {name:'somebody',gender:'male',age:28}">
        <ez-namecard data="sb"></ez-namecard>
    </body>
    </html>

好在AngularJS的scope对象直接支持对数据变化的监听。$watch方法要求传入两个参数:

    angular.module("ezstuff",[])
    .directive("ezNamecard",function($rootScope){
        return {
            restrict : "E",
            template : "<div></div>",
            link : function(scope,element,attrs){
                element.append("<div>name : <span class='name'></span></div>")
                    .append("<div>gender : <span field='gender'></span></div>")
                    .append("<div>age : <span field='age'></span></div>")
                scope.$watch(attrs.data,function(nv,ov){
                    var fields = element.find("span");
                    fields[0].innerText = nv.name;
                    fields[1].innerText = nv.gender;
                    fields[2].innerText = nv.age;
                },true)
            }
        };
    });
  1. 要监听的表达式,比如:sb
  2. 变化发生时的回调函数,AngularJS将向这个函数传入新值和旧值
    经过改进后的代码,当数据被改变时,界面会自动得到更新。这时,我们称,建立了从 数据到界面的单向绑定。

如何修改数据

一旦在指令解释器中可以访问模型,那么使用声明式模板实现数据修改也非常简单了。

我们定义一个新的指令:ez-namecard-editor,意图让其展开成这样:

    <div>
        <div>name : <input type="text"> </div>
        <div>gender : <input type="text"> </div>
        <div>age : <input type="text"></div>
    </div>

先看一下HTML的内容。使用ez-namecard-editor指令代替了ez-namecard,其他都一样。

    <html>
    <head>
        <script src="http://lib.sinaapp.com/js/angular.js/angular-1.2.19/angular.min.js"></script>
    </head>
    <body ng-app="ezstuff" ng-init="sb = {name:'somebody',gender:'male',age:28}">
        <ez-namecard-editor data="sb"></ez-namecard-editor>
    </body>
    </html>

再看JavaScript代码。在ez-namecard-editor的指令实现中,为了用input中的值自动更新 sb变量中的值,我们需要在解释器中给input对象挂接上监听函数(示例中使用keyup事件), 然后在事件中修改sb变量的对应属性就可以了。

    angular.module("ezstuff",[])
    .directive("ezNamecardEditor",function($rootScope){
        return {
            restrict : "E",
            template : "<div></div>",
            link : function(scope,element,attrs){
                var model = attrs.data;
                element.append("<div>name : <input type='text' field='name'></div>")
                    .append("<div>gender : <input type='text' field='gender'></div>")
                    .append("<div>age : <input type='text' field='age'></div>");
                element.find("input").on("keyup",function(ev){
                    var field = ev.target.getAttribute("field");
                    console.log([model,field]);
                    scope[model][field] = ev.target.value;
                })
            }
        };
    });

最终的效果是,用户在界面上进行的操作,自动地同步到了我们的数据。这时,我们称, 已经建立了从界面到数据的单向绑定。

数据变化的传播

我们已经分别实现了两个方向的绑定:

数据 → 界面:我们使用scope对象的$watch方法监听数据的变化,来更新界面。
界面 → 数据:我们在界面的DOM对象上监听变化事件,来更新数据。
看起来似乎是这样:

一个推测:

如果我们把ez-namecard和ez-namecard-editor都绑定到同一个sb对象上,
那么在ez-namecard-editor上进行编辑,将导致sb对象发生变化
由于ez-namecard监听了这个变化,所以,ez-namecard的显示也应该变化。
如我们所料吗?

合并的代码已经置入→_→,试试看!

变化传播的实现原理

scope维护了一个内部的监听队列,每次当我们在scope上执行一次$watch方法,就相当于 向这个监听队列里塞入一个监听函数。

为了捕捉对数据的修改,AngularJS要求开发者使用scope对象的applyapply方法内部会自动地调用监听队列里的监听函数:比如:

    /*
        修改scope上的sb对象的name属性
    */
    //方法1:直接修改sb对象. 不会自动触发监听函数
    scope.sb.name = 'Tonny';

    //方法2:使用scope的$apply方法,在数据修改后会自动触发监听函数
    scope.$apply("sb.name = 'Tonny'");

    //方法3:直接修改sb对象,然后调用$apply方法来传播变化。
    scope.sb.name = 'Tonny';
    scope.$apply("");

在有些情况下,AngularJS会自动调用applyapply方法的调用而被激活,如果 AngularJS没有获得一个机会来调用$apply,就需要你手工的调用它。

数据和模板解耦

在AngularJS中,界面是运转起来的声明式模板,称为视图/View,真实的数据模型/Model 通常不完全适用于某个特定应用场景的视图,所以用于视图绑定的模型,称为视图模型/ViewModel, 这些结合起来,被称为M.V.VM,即MVVM模式。

使用AngularJS进行前端开发,在建立声明式模板之后,在作用域上定义适应模板的 数据模型,然后,指令的解释器会把数据和模板关联起来,这被称为数据绑定。

数据绑定包含两个方向:

数据的变化会自动更新界面
界面的交互会自动更新数据
这个神奇的绑定不是从天而降的,脏活一点没少,只是放到了指令的解释器里。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注